diff --git a/.claude/hooks/block-risky-bash.sh b/.claude/hooks/block-risky-bash.sh new file mode 100755 index 00000000..0a68d6b9 --- /dev/null +++ b/.claude/hooks/block-risky-bash.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# PreToolUse Bash guard: block dangerous shell commands. +# Denies: rm -rf, git reset --hard, git push -f, git clean -fd, chmod -R 777, +# fork bombs, block device overwrites, mkfs, building Rust on macOS for Linux. +set -euo pipefail + +INPUT=$(cat) +CMD=$(python3 -c " +import json, sys +try: + data = json.loads(sys.stdin.read()) + print(data.get('tool_input', {}).get('command', '')) +except: pass +" <<< "$INPUT") +BASE="${CLAUDE_PROJECT_DIR:-}" +[[ -z "$BASE" ]] && BASE=$(python3 -c " +import json, sys +try: + data = json.loads(sys.stdin.read()) + print(data.get('cwd', '')) +except: pass +" <<< "$INPUT") +[[ -z "$BASE" ]] && BASE="$(pwd)" + +# Normalize: collapse whitespace, strip leading/trailing +CMD_NORM=$(echo "$CMD" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + +deny() { + local reason="$1" + python3 -c " +import json +print(json.dumps({ + 'hookSpecificOutput': { + 'hookEventName': 'PreToolUse', + 'permissionDecision': 'deny', + 'permissionDecisionReason': '$reason' + } +})) +" + exit 0 +} + +# Dangerous patterns +case "$CMD_NORM" in + *"rm -rf"*|*"rm -fr"*|*"rm -f -r"*|*"rm -r -f"*) deny "Destructive rm -rf blocked by security hook" ;; + *"git reset --hard"*) deny "git reset --hard would lose uncommitted work" ;; + *"git push --force"*|*"git push -f"*|*"git push -f "*) deny "git push --force would rewrite history" ;; + *"git clean -fd"*|*"git clean -f -d"*) deny "git clean -fd deletes untracked files" ;; + *"chmod -R 777"*|*"chmod -R 0777"*) deny "chmod -R 777 is a security risk" ;; + *":(){ :"*"};:"*) deny "Fork bomb pattern blocked" ;; + *"> /dev/sd"*|*">/dev/sd"*) deny "Block device overwrite blocked" ;; + *"mkfs "*|*"mkfs."*) deny "Disk format command blocked" ;; +esac + +# Block building Rust locally on macOS (should always build on dev server) +if [[ "$(uname)" == "Darwin" ]]; then + if echo "$CMD_NORM" | grep -qE '^\s*cargo\s+build'; then + # Allow if it's clearly an SSH command (building on remote) + if ! echo "$CMD_NORM" | grep -qE 'ssh|sshpass'; then + deny "NEVER build Rust on macOS — use ./scripts/deploy-to-target.sh --live or build on dev server via SSH" + fi + fi +fi + +# Check for path traversal escaping project root +if [[ -n "$BASE" ]] && [[ -d "$BASE" ]]; then + if echo "$CMD_NORM" | grep -qE '\.\./|/\.\.'; then + if echo "$CMD_NORM" | grep -qE '(rm|mv|cp|cat|chmod|chown)\s+.*\.\.'; then + if echo "$CMD_NORM" | grep -qE '\brm\b.*\.\.'; then + deny "Path traversal with rm blocked" + fi + fi + fi +fi + +exit 0 diff --git a/.claude/hooks/post-deploy-check.sh b/.claude/hooks/post-deploy-check.sh new file mode 100755 index 00000000..ea49b0ac --- /dev/null +++ b/.claude/hooks/post-deploy-check.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# PostToolUse Bash hook: detect deploy commands and remind to test. +# Triggers after deploy-to-target.sh runs. +set -euo pipefail + +INPUT=$(cat) + +CMD=$(python3 -c " +import json, sys +try: + data = json.loads(sys.stdin.read()) + print(data.get('tool_input', {}).get('command', '')) +except: pass +" <<< "$INPUT") + +# Only trigger on deploy commands or git push +if ! echo "$CMD" | grep -qE 'deploy-to-target|git\s+push'; then + exit 0 +fi + +TIMESTAMP=$(date '+%Y-%m-%d %H:%M') + +python3 -c " +import json + +message = '''Deploy detected at $TIMESTAMP. + +Post-deploy checklist: +1. Test the web UI at http://192.168.1.228 +2. Verify modified apps load correctly +3. Check backend logs: sudo journalctl -u archipelago -n 20 +4. Check nginx: sudo tail -f /var/log/nginx/error.log +5. If building ISO, sync system configs to image-recipe/configs/ +6. Update CHANGELOG.md if this is a notable change''' + +output = { + 'hookSpecificOutput': { + 'hookEventName': 'PostToolUse', + 'deployReminder': message + } +} +print(json.dumps(output)) +" diff --git a/.claude/hooks/protect-files.sh b/.claude/hooks/protect-files.sh new file mode 100755 index 00000000..3ff5453a --- /dev/null +++ b/.claude/hooks/protect-files.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# PreToolUse Edit|Write guard: block edits outside project and to protected paths. +# Denies: paths outside project, .git/, .env*, lockfiles, node_modules/, deploy-config.sh +set -euo pipefail + +INPUT=$(cat) +FILE_PATH=$(python3 -c " +import json, sys +try: + data = json.loads(sys.stdin.read()) + print(data.get('tool_input', {}).get('file_path', '')) +except: pass +" <<< "$INPUT") +BASE="${CLAUDE_PROJECT_DIR:-}" +[[ -z "$BASE" ]] && BASE=$(python3 -c " +import json, sys +try: + data = json.loads(sys.stdin.read()) + print(data.get('cwd', '')) +except: pass +" <<< "$INPUT") +[[ -z "$BASE" ]] && BASE="$(pwd)" + +# Resolve to absolute path +if [[ -z "$FILE_PATH" ]]; then + exit 0 +fi +ABS_BASE=$(cd "$BASE" 2>/dev/null && pwd) || true +[[ -z "$ABS_BASE" ]] && ABS_BASE=$(python3 -c "import os,sys; print(os.path.abspath(os.path.normpath(sys.argv[1])))" "$BASE" 2>/dev/null) || true +[[ -z "$ABS_BASE" ]] && ABS_BASE="$BASE" +[[ "$ABS_BASE" != */ ]] && ABS_BASE="${ABS_BASE}/" +if [[ "$FILE_PATH" != /* ]]; then + ABS_PATH="$ABS_BASE${FILE_PATH#./}" +else + ABS_PATH="$FILE_PATH" +fi +ABS_PATH=$(python3 -c "import os,sys; print(os.path.abspath(os.path.normpath(sys.argv[1])))" "$ABS_PATH" 2>/dev/null) || true +[[ -z "$ABS_PATH" ]] && ABS_PATH="$ABS_BASE${FILE_PATH#./}" + +deny() { + local reason="$1" + echo "Blocked: $ABS_PATH — $reason" >&2 + python3 -c " +import json +print(json.dumps({ + 'hookSpecificOutput': { + 'hookEventName': 'PreToolUse', + 'permissionDecision': 'deny', + 'permissionDecisionReason': '$reason' + } +})) +" + exit 0 +} + +# Protected patterns +PROTECTED_PATTERNS=( + ".git/" + ".env" + ".env.local" + "node_modules/" + "package-lock.json" + "scripts/deploy-config.sh" +) + +for pattern in "${PROTECTED_PATTERNS[@]}"; do + if [[ "$ABS_PATH" == *"$pattern"* ]] || [[ "$ABS_PATH" == *"/$pattern" ]]; then + deny "Edit blocked: path matches protected pattern ($pattern)" + fi +done + +# .env.*.local +if [[ "$ABS_PATH" =~ \.env\..*\.local$ ]]; then + deny "Edit blocked: .env.*.local files contain secrets" +fi + +# Ensure path is under project root +if [[ "$ABS_PATH" != "$ABS_BASE"* ]] && [[ "$ABS_PATH" != "$BASE"* ]]; then + deny "Edit blocked: path is outside project directory" +fi + +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..de292d7f --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,35 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-risky-bash.sh" + } + ] + }, + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-deploy-check.sh" + } + ] + } + ] + } +} diff --git a/apps/aiui/manifest.yml b/apps/aiui/manifest.yml new file mode 100644 index 00000000..2c5fad2f --- /dev/null +++ b/apps/aiui/manifest.yml @@ -0,0 +1,35 @@ +app: + id: aiui + name: AI Assistant + version: 0.1.0 + description: Conversational AI interface for Archipelago. Quarantined — communicates only via context broker. + internal: true # System-managed, not shown in App Store + + container: + image: localhost/archipelago-aiui:latest + pull_policy: always + + resources: + cpu_limit: 1 + memory_limit: 512Mi + disk_limit: 1Gi + + security: + capabilities: [] + readonly_root: true + no_new_privileges: true + network_policy: isolated # No outbound network — all data comes via context broker + + ports: + - host: 5180 + container: 80 + protocol: tcp + bind: 127.0.0.1 # Only accessible via nginx proxy, not externally + + health_check: + type: http + endpoint: http://localhost:80 + path: / + interval: 60s + timeout: 5s + retries: 3 diff --git a/image-recipe/configs/nginx-archipelago.conf b/image-recipe/configs/nginx-archipelago.conf index ca6caa36..f929ee54 100644 --- a/image-recipe/configs/nginx-archipelago.conf +++ b/image-recipe/configs/nginx-archipelago.conf @@ -6,6 +6,12 @@ server { root /opt/archipelago/web-ui; index index.html; + # AIUI SPA (Chat mode iframe) + location /aiui/ { + alias /opt/archipelago/web-ui/aiui/; + try_files $uri $uri/ /aiui/index.html; + } + # Serve static files (Vue.js SPA) location / { try_files $uri $uri/ /index.html; diff --git a/loop/loop.sh b/loop/loop.sh new file mode 100755 index 00000000..bdb478f5 --- /dev/null +++ b/loop/loop.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env sh +# Headless loop script for overnight Claude Code automation. +# Set CLAUDE_AUTONOMOUS=1 for Ralph Wiggum (Stop hook blocks until plan is complete). +# Rate-limit aware: detects limits, sleeps until reset, and retries automatically. +set -u + +PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" +PROMPT_FILE="${PROMPT_FILE:-$PROJECT_DIR/loop/prompt.md}" +LOG_FILE="${LOG_FILE:-$PROJECT_DIR/loop/loop.log}" +ITERATION_COUNT="${ITERATION_COUNT:-10}" +ITERATION_DELAY="${ITERATION_DELAY:-30}" +CLAUDE_BIN="${CLAUDE_BIN:-claude}" +RATE_LIMIT_WAIT="${RATE_LIMIT_WAIT:-3600}" +MAX_RATE_LIMIT_RETRIES="${MAX_RATE_LIMIT_RETRIES:-5}" +CLAUDE_EXIT=0 + +cd "$PROJECT_DIR" + +log() { + echo "$1" | tee -a "$LOG_FILE" +} + +banner() { + log "" + log "================================================================" + log " $1" + log " $(date '+%Y-%m-%d %H:%M:%S')" + log "================================================================" + log "" +} + +section() { + log "" + log "----------------------------------------" + log " $1" + log "----------------------------------------" + log "" +} + +plan_has_tasks() { + grep -q '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null +} + +remaining_tasks() { + grep -c '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null || echo "0" +} + +next_task() { + grep -m1 '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null | sed 's/^- \[ \] //' || echo "(none)" +} + +check_rate_limit() { + [ "${CLAUDE_EXIT:-0}" -eq 0 ] && return 1 + tail -50 "$LOG_FILE" 2>/dev/null | grep -v "^Rate limit detected" | grep -v "^Sleeping" | grep -v "^=" | grep -v "^-" | grep -qi \ + -e "rate.limit" \ + -e "too.many.requests" \ + -e "429" \ + -e "quota.exceeded" \ + -e "usage.limit" \ + -e "limit.reached" 2>/dev/null +} + +banner "ARCHY OVERNIGHT AUTOMATION STARTED" +log " Project: $PROJECT_DIR" +log " Prompt: $PROMPT_FILE" +log " Autonomous: ${CLAUDE_AUTONOMOUS:-0}" +log " Iterations: $ITERATION_COUNT (${ITERATION_DELAY}s between each)" +log " Rate limit: wait ${RATE_LIMIT_WAIT}s, retry up to ${MAX_RATE_LIMIT_RETRIES}x" +log " Tasks left: $(remaining_tasks)" +log " Next task: $(next_task)" +log "" + +i=1 +rate_limit_retries=0 +while [ "$i" -le "$ITERATION_COUNT" ]; do + + if ! plan_has_tasks; then + banner "ALL TASKS COMPLETE" + log " No remaining tasks in plan.md. Stopping." + break + fi + + section "ITERATION $i/$ITERATION_COUNT" + log " Tasks remaining: $(remaining_tasks)" + log " Next task: $(next_task)" + log "" + + export CLAUDE_PROJECT_DIR="$PROJECT_DIR" + export CLAUDE_AUTONOMOUS="${CLAUDE_AUTONOMOUS:-1}" + + if [ -f "$PROMPT_FILE" ]; then + log " Starting Claude session..." + log "" + "$CLAUDE_BIN" -p --dangerously-skip-permissions \ + < "$PROMPT_FILE" 2>&1 | tee -a "$LOG_FILE" + CLAUDE_EXIT=$? + log "" + log " Claude exited with code: $CLAUDE_EXIT" + else + log " ERROR: $PROMPT_FILE not found" + exit 1 + fi + + if check_rate_limit; then + rate_limit_retries=$((rate_limit_retries + 1)) + if [ "$rate_limit_retries" -ge "$MAX_RATE_LIMIT_RETRIES" ]; then + section "RATE LIMITED — SCHEDULING LAUNCHD RETRY" + log " Hit rate limit $rate_limit_retries times. Creating launchd job to retry later." + + PLIST_LABEL="com.archy.overnight-retry" + PLIST_PATH="$HOME/Library/LaunchAgents/${PLIST_LABEL}.plist" + RETRY_TIME=$(date -v+${RATE_LIMIT_WAIT}S '+%H:%M' 2>/dev/null || date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M') + RETRY_HOUR=$(echo "$RETRY_TIME" | cut -d: -f1) + RETRY_MIN=$(echo "$RETRY_TIME" | cut -d: -f2) + + cat > "$PLIST_PATH" < + + + + Label + ${PLIST_LABEL} + ProgramArguments + + /bin/sh + -c + cd ${PROJECT_DIR} && caffeinate -i ./loop/loop.sh >> ${LOG_FILE} 2>&1; launchctl unload ${PLIST_PATH}; rm -f ${PLIST_PATH} + + StartCalendarInterval + + Hour + ${RETRY_HOUR} + Minute + ${RETRY_MIN} + + EnvironmentVariables + + CLAUDE_AUTONOMOUS + 1 + CLAUDE_PROJECT_DIR + ${PROJECT_DIR} + PATH + /usr/local/bin:/usr/bin:/bin:$HOME/.local/bin + + StandardOutPath + ${LOG_FILE} + StandardErrorPath + ${LOG_FILE} + + +PLIST + + launchctl load "$PLIST_PATH" 2>/dev/null || true + log " Scheduled retry at ~${RETRY_TIME}" + log " Plist: $PLIST_PATH (auto-removes after running)" + exit 0 + fi + + section "RATE LIMITED — WAITING" + log " Attempt $rate_limit_retries/$MAX_RATE_LIMIT_RETRIES" + log " Sleeping ${RATE_LIMIT_WAIT}s until $(date -v+${RATE_LIMIT_WAIT}S '+%H:%M:%S' 2>/dev/null || date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M:%S')..." + sleep "$RATE_LIMIT_WAIT" + + if ! plan_has_tasks; then + banner "ALL TASKS COMPLETE (during rate limit wait)" + break + fi + log " Retrying..." + continue + fi + + rate_limit_retries=0 + + section "ITERATION $i COMPLETE" + log " Tasks remaining: $(remaining_tasks)" + log " Next task: $(next_task)" + + i=$((i + 1)) + if [ "$i" -le "$ITERATION_COUNT" ] && [ "$ITERATION_DELAY" -gt 0 ]; then + log " Pausing ${ITERATION_DELAY}s before next iteration..." + sleep "$ITERATION_DELAY" + fi +done + +banner "LOOP FINISHED" +log " Completed $((i - 1)) iterations" +log " Tasks remaining: $(remaining_tasks)" +log "" diff --git a/loop/plan.md b/loop/plan.md new file mode 100644 index 00000000..4400de33 --- /dev/null +++ b/loop/plan.md @@ -0,0 +1,79 @@ +# Overnight Plan — AIUI ↔ Archy Full Integration + +> **Format**: `- [ ]` = pending, `- [x]` = done. +> Make at least 30 attempts on any difficult task before moving on. Loop reads this file. +> **Coordination**: A separate AIUI agent handles AIUI-side changes. This plan covers Archy-side only. + +## Phase 1: Expand Protocol & Context Categories + +The current protocol only has 5 categories (apps, system, network, wallet, files). We need to add media, search, and local AI categories so AIUI can access the node's full capabilities. + +- [ ] **T1** — Expand `aiui-protocol.ts` with new context categories. Add to `AIContextCategory` type: `'media'` (local media libraries — films, songs, podcasts from Plex/Jellyfin/Navidrome), `'search'` (SearXNG metasearch on the node), `'ai-local'` (Ollama local LLM info — available models, status), `'notes'` (user notes/documents), `'bitcoin'` (Bitcoin Core chain info — block height, sync status, mempool). Add corresponding request/response types. Keep the existing 5 categories unchanged. + +- [ ] **T2** — Expand `aiPermissions.ts` with new categories. Add entries to `AI_PERMISSION_CATEGORIES` for each new category with user-friendly descriptions: media ("Local media libraries — film, music, podcast titles and metadata, no file paths"), search ("Web search via your private SearXNG instance"), ai-local ("Local AI models via Ollama — model names and availability"), notes ("Document and note titles — no contents"), bitcoin ("Bitcoin node status — block height, sync progress, mempool stats, no wallet keys"). All default OFF. + +- [ ] **T3** — Update `Settings.vue` AI Data Access section. Add toggle rows for all new categories with appropriate SVG icons. Follow the existing pattern exactly — icon, label, description, toggle switch. Group them logically: Node Data (apps, system, network, bitcoin), Media & Files (media, files, notes), AI & Search (search, ai-local), Financial (wallet). + +- [ ] **TEST:P1** — Run `cd neode-ui && npm run type-check && npm run build`. Fix all errors. Deploy: `./scripts/deploy-to-target.sh --live`. + +## Phase 2: Wire Real Data into ContextBroker + +Currently `wallet` and `files` return placeholders. Wire up real data from stores and RPC for all categories. + +- [ ] **T4** — Wire `apps` category with full data. Currently returns basic app list. Enhance to include: app version, health status, port/URL for launching, whether app has a web UI. Read from `useAppStore().packages` and `useContainerStore()`. Sanitize: strip internal IPs (replace with relative paths like `/apps/btcpay-server/`), strip env vars, strip volume paths. + +- [ ] **T5** — Wire `system` category with real metrics. Fetch from `rpcClient.call('server.metrics')` and `rpcClient.call('server.time')`. Return: CPU usage %, RAM used/total, disk used/total, uptime, OS version. Sanitize: strip hostname, kernel version details, internal IPs. + +- [ ] **T6** — Wire `network` category with real data. Fetch peer count from `rpcClient.call('node-list-peers')`. Return: peer count, Tor status (connected/not, but NOT the .onion address), whether Tailscale is active. Sanitize: strip all IPs, onion addresses, pubkeys. + +- [ ] **T7** — Wire `bitcoin` category (NEW). Fetch from Bitcoin Core RPC if the bitcoin-core package is installed and running. Check `useAppStore().packages` for bitcoin-core status. If running, call the backend RPC to get: block height, sync progress %, mempool size, network (mainnet/testnet). If not installed/stopped, return `{ available: false, message: 'Bitcoin Core not running' }`. Sanitize: no peer IPs, no wallet data. + +- [ ] **T8** — Wire `media` category (NEW). This is the content handshake. Check which media apps are installed (Plex, Jellyfin, Navidrome, Nextcloud). For each running media app, query its API through the backend to get library summaries: film count + recent titles, song/album count + recent, podcast count. Return a structured object: `{ libraries: [{ source: 'plex', type: 'film', count: N, recent: [{title, year}] }] }`. If no media apps installed, return `{ available: false, libraries: [], message: 'No media apps installed. Install Plex or Jellyfin from the App Store.' }`. Sanitize: no file paths, no internal URLs. + +- [ ] **T9** — Wire `files` category with real data. If Nextcloud or the built-in file manager is available, list top-level folders and recent files (name + type + size, no contents). If Cloud storage route exists in the app, pull from that store. Return: `{ folders: [{name, itemCount}], recentFiles: [{name, type, size, modified}] }`. Sanitize: no absolute paths, no file contents. + +- [ ] **T10** — Wire `search` category (NEW). Check if SearXNG is installed and running. If yes, return `{ available: true, engine: 'searxng', endpoint: '/apps/searxng/' }` so AIUI knows it can proxy web searches through the node. If not, return `{ available: false }`. This tells AIUI whether to use its own search or the node's private search. + +- [ ] **T11** — Wire `ai-local` category (NEW). Check if Ollama is installed and running. If yes, query for available models (model names, sizes, quantization). Return: `{ available: true, models: [{name, size, quantization}] }`. If not, return `{ available: false }`. This lets AIUI offer local AI as a provider option. + +- [ ] **T12** — Wire `wallet` category with real data. If LND is installed and running, fetch basic wallet info through backend RPC: confirmed balance (sats), channel count, total inbound/outbound capacity. If not running, return `{ available: false }`. Sanitize: NO private keys, NO seed phrases, NO channel IDs, NO peer pubkeys. Only aggregate numbers. + +- [ ] **T13** — Wire `notes` category (NEW). Check if any note-taking or document apps are installed (OnlyOffice, or built-in notes if they exist). List document titles and types (PDF, doc, note). No contents. Return: `{ documents: [{title, type, modified}] }`. If no note apps, return `{ available: false }`. + +- [ ] **TEST:P2** — Run `cd neode-ui && npm run type-check && npm run build`. Fix all errors. Deploy: `./scripts/deploy-to-target.sh --live`. SSH to server and verify the deployed build loads. + +## Phase 3: Action Handlers + +Expand the ContextBroker's action handling so AIUI can trigger real operations. + +- [ ] **T14** — Add `launch-app` action. When AIUI requests `action:request` with `action: 'launch-app'`, return the app's web UI URL so AIUI can tell the user where to go (or Archy can open it). Validate appId exists and is running. + +- [ ] **T15** — Add `search-web` action. When AIUI requests a web search action, proxy it through SearXNG if available. Accept `{ action: 'search-web', params: { query: '...' } }`, call SearXNG API, return results. This lets AIUI do private web search through the node instead of external services. + +- [ ] **T16** — Add `install-app` action enhancement. The existing install action is basic. Enhance: validate app exists in marketplace, check if already installed, return progress status. Handle errors gracefully. + +- [ ] **TEST:P3** — Type-check, build, deploy. Verify on live server. + +## Phase 4: End-to-End Testing + +Test the full integration by verifying the postMessage protocol works correctly between Archy and the deployed AIUI iframe. + +- [ ] **T17** — Create integration test script. Write a test page or script that: loads the Chat view, verifies iframe loads AIUI, sends test context requests for each category, verifies responses come back with correct structure. Can be a simple HTML page at `/test-aiui.html` or a Vue component at `/dashboard/test-aiui`. Log results to console. + +- [ ] **T18** — Test each context category end-to-end. For each of the 10 categories: enable permission in Settings, open Chat, verify AIUI receives the permission update, trigger a context request, verify data comes back. Document which categories return real data vs. placeholders (depends on what apps are installed on the server). + +- [ ] **T19** — Test action handlers. Test `navigate`, `open-app`, `launch-app`, `search-web` actions from within the AIUI iframe. Verify Archy responds correctly and performs the action. + +- [ ] **T20** — Test permission denial. Disable all permissions, open Chat, verify AIUI receives empty permissions list. Verify context requests return `{ permitted: false }`. Verify AIUI handles this gracefully (should show "Enable X access in Settings" messages). + +- [ ] **TEST:P4** — Final build, deploy, verify all tests pass on live server. + +## Phase 5: UX Polish & Deploy + +- [ ] **T21** — Add loading state to Chat.vue iframe. Show a glass-card loading indicator while AIUI iframe is loading. Listen for the `ready` postMessage from AIUI to know when it's loaded, then hide the loader. Use existing glass styling. + +- [ ] **T22** — Add connection status indicator. Small pill/dot in the Chat close button area showing whether the ContextBroker has an active connection to AIUI (received `ready` message). Green dot = connected, no dot = loading. + +- [ ] **T23** — Final deploy and smoke test. Clean build both AIUI and Archy. Deploy both. Hard refresh on 192.168.1.228. Test: login → open chat → 3 panels animate in → close → panels animate out → dashboard returns. Verify all permissions toggles work in Settings. Verify Cmd+3 opens chat, Cmd+1/2 returns to dashboard. + +- [ ] **TEST:FINAL** — Run `cd neode-ui && npm run type-check && npm run build`. Deploy with `./scripts/deploy-to-target.sh --live`. Also rebuild and deploy AIUI: `cd /Users/dorian/Projects/AIUI && rm -rf .turbo packages/app/.turbo packages/core/.turbo packages/app/dist packages/core/dist && VITE_BASE_PATH=/aiui/ pnpm build` then `sshpass -p 'EwPDR8q45l0Upx@' scp -o StrictHostKeyChecking=no -r /Users/dorian/Projects/AIUI/packages/app/dist/* archipelago@192.168.1.228:/opt/archipelago/aiui/`. Verify at http://192.168.1.228. diff --git a/loop/prepare.sh b/loop/prepare.sh new file mode 100755 index 00000000..ce02f489 --- /dev/null +++ b/loop/prepare.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env sh +# Pre-run script: verify repo state and create overnight branch. +set -eu + +PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" +cd "$PROJECT_DIR" + +DATE=$(date '+%Y-%m-%d') +BRANCH="overnight/${DATE}" + +echo "=== Archy overnight pre-run check @ $(date '+%Y-%m-%dT%H:%M:%S') ===" + +# 1. Check git status is clean +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "Error: Working tree not clean. Commit or stash changes first." >&2 + git status --short >&2 + exit 1 +fi + +# 2. Check we're not already on an overnight branch +current=$(git branch --show-current 2>/dev/null || true) +if [ -n "$current" ] && [ "$current" = "$BRANCH" ]; then + echo "Already on $BRANCH. Ready to run." >&2 + exit 0 +fi + +# 3. Create date-stamped branch +if git rev-parse --verify "$BRANCH" >/dev/null 2>&1; then + echo "Branch $BRANCH already exists. Checkout or use a different date." >&2 + exit 1 +fi +git checkout -b "$BRANCH" +echo "Created branch $BRANCH" + +echo "" +echo "Reminder: Push before starting overnight run: git push -u origin $BRANCH" +echo "Then run: caffeinate -i ./loop/loop.sh" +echo "=== Ready ===" diff --git a/loop/prompt.md b/loop/prompt.md new file mode 100644 index 00000000..b3346c34 --- /dev/null +++ b/loop/prompt.md @@ -0,0 +1,73 @@ +You are integrating AIUI (AI chat interface) into Archipelago (Archy) as its Chat mode. Read these files first: + +1. `loop/plan.md` — Your task checklist (mark items `- [x]` as you complete them) +2. `CLAUDE.md` — Archy project conventions, architecture, coding standards +3. `/Users/dorian/Projects/AIUI/CLAUDE.md` — AIUI conventions and Archy integration rules + +## Architecture Overview + +AIUI runs in an iframe at `/dashboard/chat`. Communication happens via `window.postMessage()` through a ContextBroker (Archy side) and archyBridge (AIUI side). AIUI is quarantined — it never directly accesses Archy APIs. + +``` +AIUI (iframe) ←→ postMessage ←→ ContextBroker (Archy) ←→ Pinia stores / RPC +``` + +## Key Files — Archy Side + +- `neode-ui/src/services/contextBroker.ts` — Message handler, permission checks, data fetching/sanitization +- `neode-ui/src/types/aiui-protocol.ts` — TypeScript types for postMessage protocol +- `neode-ui/src/stores/aiPermissions.ts` — User permission toggles (what AIUI can access) +- `neode-ui/src/views/Chat.vue` — Iframe container with close button +- `neode-ui/src/views/Settings.vue` — AI permissions UI section +- `neode-ui/src/api/rpc-client.ts` — Backend RPC endpoints +- `neode-ui/src/api/container-client.ts` — Container operations +- `neode-ui/src/stores/app.ts` — Main app state (packages, server info, metrics) + +## Key Files — AIUI Side (read-only reference, AIUI agent handles these) + +- `/Users/dorian/Projects/AIUI/packages/app/src/services/archyBridge.ts` — AIUI's postMessage client +- `/Users/dorian/Projects/AIUI/packages/app/src/composables/useArchy.ts` — Vue composable wrapping archyBridge +- `/Users/dorian/Projects/AIUI/packages/app/src/composables/contentExtraction.ts` — Content tag extraction pipeline +- `/Users/dorian/Projects/AIUI/packages/app/src/composables/useContentPanel.ts` — Content panel state + +## Coordination with AIUI Agent + +A separate Claude agent is working on the AIUI repo simultaneously. Your job is the **Archy side only**: +- Expand the ContextBroker to serve real data for all categories +- Add new context categories for media, search, and local AI +- Wire up real store/RPC data instead of placeholders +- Deploy and test on the live server at 192.168.1.228 +- DO NOT edit files in /Users/dorian/Projects/AIUI/ — the other agent handles that + +## Content Handshake Protocol + +AIUI's content pipeline uses `[[tag:data]]` syntax in AI responses to surface content. The AI needs context about what's available on the node to generate these tags. The handshake works like this: + +1. AIUI sends `context:request` with category (e.g., `media`, `apps`, `files`) +2. Archy's ContextBroker checks permissions, fetches from stores/RPC, sanitizes +3. Returns data to AIUI which injects it into the AI's system prompt +4. AI generates responses with appropriate `[[film:id]]`, `[[song:id]]` tags referencing actual library content +5. AIUI's content extraction pipeline renders the tagged content in panels + +## For each task in loop/plan.md: + +1. Find the first unchecked `- [ ]` item +2. Read the task description carefully +3. Read the relevant source files before making changes +4. Implement following CLAUDE.md conventions (glass styling, TypeScript strict, etc.) +5. Run `cd neode-ui && npm run type-check` — fix all errors before continuing +6. Run `cd neode-ui && npm run build` — must succeed +7. Deploy to live server: `./scripts/deploy-to-target.sh --live` +8. Commit: `type: description` (conventional commits) +9. Mark it done `- [x]` in `loop/plan.md` +10. Move to the next unchecked task immediately + +## Rules + +- Never skip a build/typecheck gate — if it fails, fix before moving on +- If a task is proving difficult, make at least 30 genuine attempts before moving on +- Always deploy after completing a task — changes must be live at 192.168.1.228 +- Do NOT edit AIUI files — only Archy files +- Build AIUI when needed: `cd /Users/dorian/Projects/AIUI && rm -rf .turbo packages/app/.turbo packages/core/.turbo packages/app/dist packages/core/dist && VITE_BASE_PATH=/aiui/ pnpm build` +- Deploy AIUI dist: `sshpass -p 'EwPDR8q45l0Upx@' scp -o StrictHostKeyChecking=no -r /Users/dorian/Projects/AIUI/packages/app/dist/* archipelago@192.168.1.228:/opt/archipelago/aiui/` +- Do not stop until all tasks are checked or you are rate limited diff --git a/neode-ui/package.json b/neode-ui/package.json index 88bdeec9..f6160863 100644 --- a/neode-ui/package.json +++ b/neode-ui/package.json @@ -7,7 +7,7 @@ "start": "./start-dev.sh", "stop": "./stop-dev.sh", "dev": "vite", - "dev:mock": "concurrently \"node mock-backend.js\" \"vite\"", + "dev:mock": "concurrently \"node mock-backend.js\" \"VITE_AIUI_URL=http://localhost:5173 vite\" \"cd ../../AIUI && pnpm dev 2>/dev/null || echo '[AIUI] Not found at ../../AIUI — chat will show placeholder'\"", "dev:real": "echo 'Start backend: cd ../core && cargo run --release' && vite", "backend:mock": "node mock-backend.js", "backend:real": "cd ../core && cargo run --release", diff --git a/neode-ui/src/App.vue b/neode-ui/src/App.vue index dfc1cead..9de8b256 100644 --- a/neode-ui/src/App.vue +++ b/neode-ui/src/App.vue @@ -77,12 +77,14 @@ import { useCLIStore } from '@/stores/cli' import { useMessageToast } from '@/composables/useMessageToast' import { useAppStore } from '@/stores/app' import { useScreensaverStore } from '@/stores/screensaver' +import { useUIModeStore } from '@/stores/uiMode' const router = useRouter() const screensaverStore = useScreensaverStore() const spotlightStore = useSpotlightStore() const cliStore = useCLIStore() const appStore = useAppStore() +const uiModeStore = useUIModeStore() const messageToast = useMessageToast() const toastMessage = messageToast.toastMessage @@ -125,6 +127,19 @@ function onKeyDown(e: KeyboardEvent) { cliStore.toggle() return } + // Cmd+1/2/3 - switch UI mode (skip when in input) + if (mod && !isInput && appStore.isAuthenticated) { + if (e.key === '1') { e.preventDefault(); uiModeStore.setMode('easy'); router.push('/dashboard'); return } + if (e.key === '2') { e.preventDefault(); uiModeStore.setMode('gamer'); router.push('/dashboard'); return } + if (e.key === '3') { e.preventDefault(); router.push('/dashboard/chat'); return } + } + // Cmd+M / Ctrl+M - cycle UI mode (skip when in input) + if (mod && (e.key === 'm' || e.key === 'M') && !isInput && appStore.isAuthenticated) { + e.preventDefault() + uiModeStore.cycleMode() + router.push('/dashboard') + return + } // 's' key activates screensaver when authenticated (skip if typing in input) if (e.key === 's' || e.key === 'S') { const target = e.target as HTMLElement diff --git a/neode-ui/src/components/ModeSwitcher.vue b/neode-ui/src/components/ModeSwitcher.vue index 7929b5e9..68decda0 100644 --- a/neode-ui/src/components/ModeSwitcher.vue +++ b/neode-ui/src/components/ModeSwitcher.vue @@ -1,5 +1,14 @@