Initial release: iai-mcp v0.1.0
Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: XNLLLLH <XNLLLLH@users.noreply.github.com>
This commit is contained in:
commit
f6b876fbe7
332 changed files with 97258 additions and 0 deletions
75
scripts/com.iai-mcp.daemon.plist.template
Normal file
75
scripts/com.iai-mcp.daemon.plist.template
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<?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">
|
||||
<!--
|
||||
socket-activated LaunchAgent template (R1, D7.1-01).
|
||||
Rendered by scripts/install.sh: substitutes {PYTHON_PATH} + {HOME},
|
||||
writes to ~/Library/LaunchAgents/com.iai-mcp.daemon.plist, then
|
||||
`launchctl load -w` registers it.
|
||||
|
||||
DO NOT edit ~/Library/LaunchAgents/com.iai-mcp.daemon.plist directly —
|
||||
re-run scripts/install.sh after changes here.
|
||||
|
||||
RunAtLoad=false + Sockets.Listener = TRUE socket activation: launchd
|
||||
pre-binds the unix socket and spawns iai_mcp.daemon ONLY on first
|
||||
connection. KeepAlive.SuccessfulExit=false preserves Phase 7's idle
|
||||
shutdown semantics (no respawn after clean idle exit).
|
||||
-->
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.iai-mcp.daemon</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{PYTHON_PATH}</string>
|
||||
<string>-m</string>
|
||||
<string>iai_mcp.daemon</string>
|
||||
</array>
|
||||
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
|
||||
<key>HOME</key>
|
||||
<string>{HOME}</string>
|
||||
<key>IAI_MCP_LAUNCHD_MANAGED</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
|
||||
<key>Sockets</key>
|
||||
<dict>
|
||||
<key>Listener</key>
|
||||
<dict>
|
||||
<key>SockPathName</key>
|
||||
<string>{HOME}/.iai-mcp/.daemon.sock</string>
|
||||
<key>SockType</key>
|
||||
<string>stream</string>
|
||||
<key>SockFamily</key>
|
||||
<string>Unix</string>
|
||||
<key>SockPathMode</key>
|
||||
<integer>384</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<false/>
|
||||
|
||||
<key>KeepAlive</key>
|
||||
<dict>
|
||||
<key>SuccessfulExit</key>
|
||||
<false/>
|
||||
</dict>
|
||||
|
||||
<key>ProcessType</key>
|
||||
<string>Adaptive</string>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>{HOME}/.iai-mcp/logs/launchd-stdout.log</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{HOME}/.iai-mcp/logs/launchd-stderr.log</string>
|
||||
|
||||
<key>WorkingDirectory</key>
|
||||
<string>{HOME}</string>
|
||||
</dict>
|
||||
</plist>
|
||||
108
scripts/idle_cpu_regression_fence.sh
Executable file
108
scripts/idle_cpu_regression_fence.sh
Executable file
|
|
@ -0,0 +1,108 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/idle_cpu_regression_fence.sh — A7 idle-CPU regression fence.
|
||||
#
|
||||
# SPEC A7: 30-min `python -m iai_mcp.daemon` run with first_turn_pending = 1
|
||||
# shows process CPU < 5% sampled every 30s.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/idle_cpu_regression_fence.sh # 30-min run, samples every 30s
|
||||
# IAI_FENCE_DURATION_MIN=5 scripts/idle_cpu_regression_fence.sh # short run
|
||||
#
|
||||
# Pre-condition: daemon must already be running. The script does NOT spawn
|
||||
# the daemon and does NOT manage launchd — D7.2-26 + D7.2-27 keep daemon
|
||||
# lifecycle entirely under user discretion. To start the daemon manually
|
||||
# before running this fence, run:
|
||||
#
|
||||
# python -m iai_mcp.daemon &
|
||||
#
|
||||
# (development / manual subprocess path; foreground or background). The
|
||||
# fence treats the daemon as a black box and only reads its self-CPU% via
|
||||
# psutil, so any startup mechanism that yields a `iai_mcp.daemon` process
|
||||
# will work.
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — all samples < THRESHOLD_PCT sustained
|
||||
# 1 — at least one sample >= THRESHOLD_PCT
|
||||
# 2 — daemon not running / pgrep returned 0 matches
|
||||
# 3 — psutil / Python error
|
||||
set -eu
|
||||
|
||||
DURATION_MIN="${IAI_FENCE_DURATION_MIN:-30}"
|
||||
SAMPLE_INTERVAL_SEC="${IAI_FENCE_SAMPLE_INTERVAL_SEC:-30}"
|
||||
THRESHOLD_PCT="${IAI_FENCE_THRESHOLD_PCT:-5.0}"
|
||||
|
||||
# Locate the daemon PID. We use pgrep -f for the explicit module form.
|
||||
DAEMON_PID=$(pgrep -f "iai_mcp.daemon" | head -1 || true)
|
||||
if [ -z "$DAEMON_PID" ]; then
|
||||
echo "ERROR: no iai_mcp.daemon process found." >&2
|
||||
echo "Start it manually before running this fence:" >&2
|
||||
echo " python -m iai_mcp.daemon &" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "Phase 7.2 A7 idle-CPU regression fence"
|
||||
echo " daemon PID: $DAEMON_PID"
|
||||
echo " duration: ${DURATION_MIN}min"
|
||||
echo " sample interval: ${SAMPLE_INTERVAL_SEC}s"
|
||||
echo " threshold: ${THRESHOLD_PCT}%"
|
||||
echo
|
||||
|
||||
SAMPLES_TAKEN=0
|
||||
OVER_THRESHOLD=0
|
||||
MAX_SEEN=0
|
||||
DURATION_SEC=$((DURATION_MIN * 60))
|
||||
START_TS=$(date +%s)
|
||||
|
||||
while true; do
|
||||
NOW=$(date +%s)
|
||||
ELAPSED=$((NOW - START_TS))
|
||||
if [ $ELAPSED -ge $DURATION_SEC ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
# Use python+psutil for cross-platform self-CPU% read.
|
||||
CPU=$(python3 -c "
|
||||
import psutil, sys
|
||||
try:
|
||||
p = psutil.Process($DAEMON_PID)
|
||||
p.cpu_percent(interval=None)
|
||||
import time
|
||||
time.sleep(1.0)
|
||||
print(p.cpu_percent(interval=None))
|
||||
except Exception as e:
|
||||
sys.stderr.write(f'psutil error: {e}\n')
|
||||
sys.exit(3)
|
||||
")
|
||||
EXIT_CODE=$?
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
echo " sample fail: psutil error" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
SAMPLES_TAKEN=$((SAMPLES_TAKEN + 1))
|
||||
printf " t=%4ds cpu=%5.1f%%\n" "$ELAPSED" "$CPU"
|
||||
|
||||
# awk float comparison (bash doesn't do floats natively).
|
||||
OVER=$(awk -v cpu="$CPU" -v thr="$THRESHOLD_PCT" 'BEGIN { print (cpu > thr) ? 1 : 0 }')
|
||||
if [ "$OVER" = "1" ]; then
|
||||
OVER_THRESHOLD=$((OVER_THRESHOLD + 1))
|
||||
fi
|
||||
|
||||
MAX_SEEN=$(awk -v cur="$CPU" -v prev="$MAX_SEEN" 'BEGIN { print (cur > prev) ? cur : prev }')
|
||||
|
||||
sleep "$SAMPLE_INTERVAL_SEC"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "Summary:"
|
||||
echo " total samples: $SAMPLES_TAKEN"
|
||||
echo " over threshold: $OVER_THRESHOLD"
|
||||
echo " max observed CPU%: $MAX_SEEN"
|
||||
|
||||
if [ $OVER_THRESHOLD -gt 0 ]; then
|
||||
echo "FAIL: $OVER_THRESHOLD/$SAMPLES_TAKEN samples exceeded ${THRESHOLD_PCT}%."
|
||||
exit 1
|
||||
else
|
||||
echo "PASS: all samples under threshold."
|
||||
exit 0
|
||||
fi
|
||||
198
scripts/install.sh
Executable file
198
scripts/install.sh
Executable file
|
|
@ -0,0 +1,198 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/install.sh — first-time setup for collaborators.
|
||||
#
|
||||
# Usage (from repo root or anywhere inside the clone):
|
||||
# bash scripts/install.sh
|
||||
#
|
||||
# Does:
|
||||
# 1. creates .venv if missing
|
||||
# 2. installs iai-mcp editable into the venv
|
||||
# 3. builds the TS MCP wrapper
|
||||
# 4. symlinks ~/.local/bin/iai-mcp -> .venv/bin/iai-mcp so the CLI is
|
||||
# callable from anywhere without activating the venv
|
||||
# 5. optionally installs the sleep daemon (launchd on macOS, systemd on Linux)
|
||||
#
|
||||
# Idempotent. Safe to re-run.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
step() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }
|
||||
ok() { printf ' \033[0;32m✓\033[0m %s\n' "$*"; }
|
||||
warn() { printf ' \033[0;33m!\033[0m %s\n' "$*"; }
|
||||
die() { printf '\n\033[0;31m✗ %s\033[0m\n' "$*" >&2; exit 1; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sections 1-4: build / venv / pip / npm / symlink.
|
||||
#
|
||||
# IAI_TEST_SKIP_BUILD=1 short-circuits the whole bootstrap so the LaunchAgent
|
||||
# section (6) can be exercised in isolation by tests/test_install_uninstall.py
|
||||
# (Plan 07.1-03 Task 3) without spending ~30s on venv + npm.
|
||||
# ---------------------------------------------------------------------------
|
||||
if [[ "${IAI_TEST_SKIP_BUILD:-0}" == "1" ]]; then
|
||||
step "build skip (IAI_TEST_SKIP_BUILD=1)"
|
||||
ok "skipping sections 1-4 (venv/pip/npm/symlink) — test mode"
|
||||
else
|
||||
# -----------------------------------------------------------------------
|
||||
# 1. venv
|
||||
# -----------------------------------------------------------------------
|
||||
step "python venv"
|
||||
# iai-mcp requires Python 3.11 or 3.12 (torch + lancedb on 3.13/3.14
|
||||
# are not yet stable). Pick the highest supported interpreter we can find.
|
||||
PY=""
|
||||
for cand in python3.12 python3.11; do
|
||||
if command -v "$cand" >/dev/null 2>&1; then
|
||||
PY="$(command -v $cand)"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$PY" ]; then
|
||||
# Fall back to plain python3 only if it self-reports as 3.11 or 3.12.
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
ver="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || echo unknown)"
|
||||
if [ "$ver" = "3.11" ] || [ "$ver" = "3.12" ]; then
|
||||
PY="$(command -v python3)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
[ -n "$PY" ] || die "Python 3.11 or 3.12 not found. macOS: brew install python@3.12 | Linux: apt install python3.12 (or use pyenv)"
|
||||
ok "using $PY ($($PY --version))"
|
||||
if [ ! -d .venv ]; then
|
||||
"$PY" -m venv .venv
|
||||
ok ".venv created"
|
||||
else
|
||||
ok ".venv already exists"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 2. editable install
|
||||
# -----------------------------------------------------------------------
|
||||
step "editable install (pip -e .)"
|
||||
.venv/bin/pip install --quiet --upgrade pip
|
||||
.venv/bin/pip install --quiet -e .
|
||||
ok "iai-mcp python package installed into venv"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 3. TS wrapper build
|
||||
# -----------------------------------------------------------------------
|
||||
step "TS wrapper build"
|
||||
if [ -d mcp-wrapper ]; then
|
||||
pushd mcp-wrapper >/dev/null
|
||||
if [ -f package-lock.json ]; then
|
||||
npm ci --silent --no-audit --no-fund
|
||||
else
|
||||
npm install --silent --no-audit --no-fund
|
||||
fi
|
||||
npm run build --silent
|
||||
popd >/dev/null
|
||||
ok "mcp-wrapper/dist built"
|
||||
else
|
||||
warn "mcp-wrapper/ missing — skipping"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 4. global symlink into ~/.local/bin
|
||||
# -----------------------------------------------------------------------
|
||||
step "global CLI symlink"
|
||||
LOCAL_BIN="${HOME}/.local/bin"
|
||||
LINK_PATH="${LOCAL_BIN}/iai-mcp"
|
||||
TARGET="${REPO_ROOT}/.venv/bin/iai-mcp"
|
||||
|
||||
[ -x "${TARGET}" ] || die "venv entry point not found at ${TARGET}"
|
||||
|
||||
mkdir -p "${LOCAL_BIN}"
|
||||
|
||||
# `ln -sf` overwrites any existing symlink safely (idempotent).
|
||||
# Refuse to clobber a regular file the user put there themselves.
|
||||
if [ -e "${LINK_PATH}" ] && [ ! -L "${LINK_PATH}" ]; then
|
||||
die "${LINK_PATH} exists and is NOT a symlink. move it aside and re-run."
|
||||
fi
|
||||
ln -sf "${TARGET}" "${LINK_PATH}"
|
||||
ok "${LINK_PATH} -> ${TARGET}"
|
||||
|
||||
# PATH sanity check using python (grep is hook-blocked in this dev env).
|
||||
PATH_HAS_LOCAL_BIN="$(.venv/bin/python - <<PY
|
||||
import os
|
||||
print("1" if "${LOCAL_BIN}" in os.environ.get("PATH", "").split(":") else "0")
|
||||
PY
|
||||
)"
|
||||
if [ "${PATH_HAS_LOCAL_BIN}" != "1" ]; then
|
||||
warn "${LOCAL_BIN} is NOT in your PATH"
|
||||
warn "add this to ~/.zshrc or ~/.bashrc and restart your shell:"
|
||||
warn " export PATH=\"\${HOME}/.local/bin:\${PATH}\""
|
||||
else
|
||||
ok "${LOCAL_BIN} is in PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. optional daemon install
|
||||
# ---------------------------------------------------------------------------
|
||||
step "sleep daemon (optional)"
|
||||
if command -v iai-mcp >/dev/null 2>&1; then
|
||||
INSTALLED_PATH="$(command -v iai-mcp)"
|
||||
ok "iai-mcp globally reachable at ${INSTALLED_PATH}"
|
||||
echo
|
||||
echo " to run the background sleep daemon (recommended — REM cycles +"
|
||||
echo " overnight consolidation on your local Claude subscription):"
|
||||
echo
|
||||
echo " iai-mcp daemon install --yes"
|
||||
echo " iai-mcp daemon start"
|
||||
echo
|
||||
echo " or skip for now and install later."
|
||||
else
|
||||
warn "iai-mcp not on PATH yet — add ~/.local/bin to PATH first, then run:"
|
||||
warn " iai-mcp daemon install --yes"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. LaunchAgent registration (Phase 7.1 — socket-activated singleton)
|
||||
#
|
||||
# Section 6 — socket-activated LaunchAgent. REPLACES the eager
|
||||
# RunAtLoad=true plist that `iai-mcp daemon install` writes.
|
||||
# The two flows compete for ~/Library/LaunchAgents/com.iai-mcp.daemon.plist;
|
||||
# whichever ran most recently wins. install.sh always wins because
|
||||
# it overwrites + reloads on every invocation (idempotent by design).
|
||||
# ---------------------------------------------------------------------------
|
||||
step "LaunchAgent registration"
|
||||
if [[ "$(uname)" != "Darwin" ]]; then
|
||||
warn "non-Darwin OS — skipping LaunchAgent registration"
|
||||
elif [[ "${DRY_RUN:-0}" == "1" ]]; then
|
||||
ok "DRY_RUN=1 — skipping launchctl calls (test mode)"
|
||||
else
|
||||
PYTHON_PATH="${REPO_ROOT}/.venv/bin/python"
|
||||
if [ ! -x "${PYTHON_PATH}" ]; then
|
||||
warn "venv python not found at ${PYTHON_PATH} — falling back to $(command -v python3)"
|
||||
PYTHON_PATH="$(command -v python3)"
|
||||
fi
|
||||
LA_DIR="${HOME}/Library/LaunchAgents"
|
||||
LA_PATH="${LA_DIR}/com.iai-mcp.daemon.plist"
|
||||
TEMPLATE="${REPO_ROOT}/scripts/com.iai-mcp.daemon.plist.template"
|
||||
[ -f "${TEMPLATE}" ] || die "plist template missing at ${TEMPLATE}"
|
||||
mkdir -p "${LA_DIR}" "${HOME}/.iai-mcp/logs" "${HOME}/.iai-mcp"
|
||||
# Substitute placeholders using sed; HOME/PYTHON_PATH may contain forward
|
||||
# slashes so we use `|` as the sed separator (not `/`).
|
||||
sed -e "s|{PYTHON_PATH}|${PYTHON_PATH}|g" -e "s|{HOME}|${HOME}|g" "${TEMPLATE}" > "${LA_PATH}"
|
||||
# Idempotent: unload prior registration if any, then load fresh. -w persists across reboots.
|
||||
launchctl unload -w "${LA_PATH}" 2>/dev/null || true
|
||||
if ! launchctl load -w "${LA_PATH}"; then
|
||||
warn "launchctl load reported non-zero — checking registration anyway"
|
||||
fi
|
||||
if launchctl list | grep -q "com.iai-mcp.daemon"; then
|
||||
ok "LaunchAgent registered (first MCP call will socket-activate the daemon)"
|
||||
else
|
||||
die "LaunchAgent NOT registered after launchctl load — investigate ${HOME}/.iai-mcp/logs/launchd-stderr.log"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# done
|
||||
# ---------------------------------------------------------------------------
|
||||
step "done"
|
||||
ok "iai-mcp installed at $(git rev-parse --short HEAD)"
|
||||
echo
|
||||
echo " next: bash scripts/uninstall.sh (to roll back; preserves data unless --purge-data)"
|
||||
echo " update: bash scripts/update.sh (pull + rebuild + restart daemon)"
|
||||
189
scripts/uninstall.sh
Executable file
189
scripts/uninstall.sh
Executable file
|
|
@ -0,0 +1,189 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/uninstall.sh — LaunchAgent + daemon teardown.
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/uninstall.sh # remove LaunchAgent + kill daemon
|
||||
# bash scripts/uninstall.sh --purge-state # also remove ~/.iai-mcp/.daemon.sock,
|
||||
# # .daemon-state.json, .lock
|
||||
# bash scripts/uninstall.sh --purge-data # also remove ~/.iai-mcp/lancedb +
|
||||
# # runtime_graph_cache.json
|
||||
# # DESTRUCTIVE — wipes user's brain.
|
||||
#
|
||||
# Idempotent: safe to re-run. Always exits 0 (best-effort).
|
||||
# DRY_RUN=1 env skips real launchctl + kill + rm calls (used by tests).
|
||||
#
|
||||
# Inverse of scripts/install.sh section 6 (Phase 7.1 LaunchAgent registration).
|
||||
|
||||
# NOTE on shell flags: we deliberately use only `set -u`, NOT `set -e`.
|
||||
# Uninstall must NEVER abort mid-flow — partial cleanup is worse than
|
||||
# best-effort full cleanup. Each step prints its own outcome via ok/warn/die.
|
||||
set -u
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
step() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }
|
||||
ok() { printf ' \033[0;32m✓\033[0m %s\n' "$*"; }
|
||||
warn() { printf ' \033[0;33m!\033[0m %s\n' "$*"; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. parse flags
|
||||
# ---------------------------------------------------------------------------
|
||||
PURGE_STATE=0
|
||||
PURGE_DATA=0
|
||||
for arg in "$@"; do
|
||||
case "${arg}" in
|
||||
--purge-state) PURGE_STATE=1 ;;
|
||||
--purge-data) PURGE_DATA=1 ;;
|
||||
-h|--help)
|
||||
sed -n '2,12p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
warn "unknown flag '${arg}' (ignored — expected --purge-state, --purge-data, --help)"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
step "iai-mcp uninstall"
|
||||
if [[ "${PURGE_DATA}" == "1" ]]; then
|
||||
warn "--purge-data is DESTRUCTIVE: ~/.iai-mcp/lancedb (your brain) will be deleted"
|
||||
fi
|
||||
|
||||
LA_PATH="${HOME}/Library/LaunchAgents/com.iai-mcp.daemon.plist"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. launchctl unload (Darwin only)
|
||||
# ---------------------------------------------------------------------------
|
||||
step "launchctl unload"
|
||||
if [[ "$(uname)" != "Darwin" ]]; then
|
||||
warn "non-Darwin OS — skipping launchctl unload"
|
||||
elif [[ "${DRY_RUN:-0}" == "1" ]]; then
|
||||
ok "DRY_RUN=1 — skipping launchctl unload (test mode)"
|
||||
else
|
||||
if [ -f "${LA_PATH}" ]; then
|
||||
if launchctl unload -w "${LA_PATH}" 2>/dev/null; then
|
||||
ok "LaunchAgent unloaded"
|
||||
else
|
||||
ok "LaunchAgent was not registered (already clean)"
|
||||
fi
|
||||
else
|
||||
ok "no LaunchAgent plist at ${LA_PATH} (already clean)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. remove plist file (Darwin only)
|
||||
# ---------------------------------------------------------------------------
|
||||
step "remove plist"
|
||||
if [[ "$(uname)" != "Darwin" ]]; then
|
||||
warn "non-Darwin OS — skipping plist removal"
|
||||
elif [[ "${DRY_RUN:-0}" == "1" ]]; then
|
||||
ok "DRY_RUN=1 — skipping rm of ${LA_PATH} (test mode)"
|
||||
else
|
||||
rm -f "${LA_PATH}"
|
||||
ok "${LA_PATH} removed (or never existed)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. kill any lingering daemon by cmdline match
|
||||
#
|
||||
# Defense against pgrep regex misfire: pgrep -f matches on substring of
|
||||
# the full command line. We re-verify each PID's cmdline contains the
|
||||
# literal "iai_mcp.daemon" via `ps -p PID -o command=` BEFORE killing.
|
||||
# ---------------------------------------------------------------------------
|
||||
step "kill lingering daemon"
|
||||
if [[ "${DRY_RUN:-0}" == "1" ]]; then
|
||||
ok "DRY_RUN=1 — skipping pgrep + kill (test mode)"
|
||||
else
|
||||
pids="$(pgrep -f "iai_mcp\.daemon" 2>/dev/null || true)"
|
||||
if [[ -n "${pids}" ]]; then
|
||||
warn "found pids: ${pids}"
|
||||
for pid in ${pids}; do
|
||||
# Verify cmdline really contains iai_mcp.daemon (defense against pgrep regex misfire).
|
||||
if ps -p "${pid}" -o command= 2>/dev/null | grep -q "iai_mcp.daemon"; then
|
||||
kill -TERM "${pid}" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
sleep 3
|
||||
# SIGKILL stragglers
|
||||
for pid in ${pids}; do
|
||||
if kill -0 "${pid}" 2>/dev/null; then
|
||||
warn "pid ${pid} still alive — sending SIGKILL"
|
||||
kill -KILL "${pid}" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
ok "lingering daemon(s) terminated"
|
||||
else
|
||||
ok "no lingering iai_mcp.daemon processes"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. --purge-state: remove socket + state + lock
|
||||
# ---------------------------------------------------------------------------
|
||||
if [[ "${PURGE_STATE}" == "1" ]]; then
|
||||
step "--purge-state: remove ~/.iai-mcp/.daemon.sock + .daemon-state.json + .lock"
|
||||
if [[ "${DRY_RUN:-0}" == "1" ]]; then
|
||||
ok "DRY_RUN=1 — skipping rm of state files (test mode)"
|
||||
else
|
||||
rm -f "${HOME}/.iai-mcp/.daemon.sock" \
|
||||
"${HOME}/.iai-mcp/.daemon-state.json" \
|
||||
"${HOME}/.iai-mcp/.lock"
|
||||
ok "state files removed (or never existed)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. --purge-data: remove lancedb + runtime cache (DESTRUCTIVE)
|
||||
# ---------------------------------------------------------------------------
|
||||
if [[ "${PURGE_DATA}" == "1" ]]; then
|
||||
step "--purge-data: remove ~/.iai-mcp/lancedb + runtime_graph_cache.json"
|
||||
if [[ "${DRY_RUN:-0}" == "1" ]]; then
|
||||
ok "DRY_RUN=1 — skipping rm of data files (test mode)"
|
||||
else
|
||||
# Confirmation prompt — only if attached to a tty (skip in non-interactive
|
||||
# subprocess to avoid hanging under set -u). bash 3.2 compatible.
|
||||
confirmed=0
|
||||
if [ -t 0 ]; then
|
||||
printf " \033[0;33m!\033[0m really delete ~/.iai-mcp/lancedb? [y/N] "
|
||||
read -r REPLY || REPLY=N
|
||||
if [[ "${REPLY}" =~ ^[Yy]$ ]]; then
|
||||
confirmed=1
|
||||
fi
|
||||
else
|
||||
warn "non-interactive stdin — skipping --purge-data confirmation (no deletion)"
|
||||
fi
|
||||
if [[ "${confirmed}" == "1" ]]; then
|
||||
rm -rf "${HOME}/.iai-mcp/lancedb" \
|
||||
"${HOME}/.iai-mcp/runtime_graph_cache.json"
|
||||
ok "data files removed"
|
||||
else
|
||||
ok "user declined — data preserved"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 7. verify
|
||||
# ---------------------------------------------------------------------------
|
||||
step "verify"
|
||||
if [[ "$(uname)" != "Darwin" ]]; then
|
||||
warn "non-Darwin OS — skipping launchctl verify"
|
||||
elif [[ "${DRY_RUN:-0}" == "1" ]]; then
|
||||
ok "DRY_RUN=1 — skipping launchctl list verify (test mode)"
|
||||
else
|
||||
if launchctl list 2>/dev/null | grep -q "com.iai-mcp.daemon"; then
|
||||
warn "com.iai-mcp.daemon still appears in launchctl list — manual cleanup may be needed"
|
||||
else
|
||||
ok "com.iai-mcp.daemon no longer in launchctl list"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# done
|
||||
# ---------------------------------------------------------------------------
|
||||
step "done"
|
||||
ok "iai-mcp uninstalled — re-run scripts/install.sh to restore."
|
||||
exit 0
|
||||
143
scripts/update.sh
Executable file
143
scripts/update.sh
Executable file
|
|
@ -0,0 +1,143 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/update.sh — pull + rebuild + restart daemon for collaborators
|
||||
#
|
||||
# Usage (from repo root or anywhere inside the clone):
|
||||
# bash scripts/update.sh
|
||||
#
|
||||
# Idempotent. Aborts on a dirty working tree so local changes are never
|
||||
# clobbered. Re-runs safely — each step detects whether it is needed.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Resolve repo root no matter where the script is invoked from.
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
step() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }
|
||||
ok() { printf ' \033[0;32m✓\033[0m %s\n' "$*"; }
|
||||
warn() { printf ' \033[0;33m!\033[0m %s\n' "$*"; }
|
||||
die() { printf '\n\033[0;31m✗ %s\033[0m\n' "$*" >&2; exit 1; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 0. Preconditions
|
||||
# ---------------------------------------------------------------------------
|
||||
step "preflight"
|
||||
[ -d .git ] || die "not a git repository (run from an iai-mcp clone)"
|
||||
|
||||
# Require a clean working tree — never trample local edits.
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
git status --short
|
||||
die "working tree is dirty. commit or stash first, then re-run."
|
||||
fi
|
||||
ok "working tree clean"
|
||||
|
||||
VENV_PY="${REPO_ROOT}/.venv/bin/python"
|
||||
[ -x "${VENV_PY}" ] || die ".venv/bin/python not found — run 'python3 -m venv .venv && .venv/bin/pip install -e .' once, then rerun"
|
||||
ok "venv detected"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. git pull (fast-forward only — never merge surprises)
|
||||
# ---------------------------------------------------------------------------
|
||||
step "git pull --ff-only origin main"
|
||||
BEFORE="$(git rev-parse HEAD)"
|
||||
git fetch --quiet origin main
|
||||
git pull --ff-only --quiet origin main
|
||||
AFTER="$(git rev-parse HEAD)"
|
||||
if [ "${BEFORE}" = "${AFTER}" ]; then
|
||||
ok "already at $(git rev-parse --short HEAD) — no upstream commits"
|
||||
NOOP=1
|
||||
else
|
||||
ok "advanced $(git rev-parse --short "${BEFORE}") → $(git rev-parse --short "${AFTER}")"
|
||||
NOOP=0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Python package (editable reinstall — picks up deps or entry-point drift)
|
||||
# ---------------------------------------------------------------------------
|
||||
step "python package refresh (editable)"
|
||||
"${VENV_PY}" -m pip install --quiet -e . || die "pip install -e failed"
|
||||
ok "iai-mcp python package up to date"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. TypeScript MCP wrapper
|
||||
# ---------------------------------------------------------------------------
|
||||
step "TS wrapper build"
|
||||
if [ -d mcp-wrapper ]; then
|
||||
pushd mcp-wrapper >/dev/null
|
||||
if [ -f package-lock.json ]; then
|
||||
npm ci --silent --no-audit --no-fund
|
||||
else
|
||||
npm install --silent --no-audit --no-fund
|
||||
fi
|
||||
npm run build --silent
|
||||
popd >/dev/null
|
||||
ok "mcp-wrapper/dist rebuilt"
|
||||
else
|
||||
warn "mcp-wrapper/ missing — skipping"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Global CLI symlink (idempotent — ensures ~/.local/bin/iai-mcp exists)
|
||||
# ---------------------------------------------------------------------------
|
||||
step "global CLI symlink"
|
||||
LOCAL_BIN="${HOME}/.local/bin"
|
||||
LINK_PATH="${LOCAL_BIN}/iai-mcp"
|
||||
TARGET="${REPO_ROOT}/.venv/bin/iai-mcp"
|
||||
if [ -e "${LINK_PATH}" ] && [ ! -L "${LINK_PATH}" ]; then
|
||||
warn "${LINK_PATH} exists as a regular file — skipping symlink refresh"
|
||||
else
|
||||
mkdir -p "${LOCAL_BIN}"
|
||||
ln -sf "${TARGET}" "${LINK_PATH}"
|
||||
ok "${LINK_PATH} -> ${TARGET}"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Daemon (restart only if currently running; plist drift advisory)
|
||||
# ---------------------------------------------------------------------------
|
||||
step "daemon lifecycle"
|
||||
IAI_MCP="${REPO_ROOT}/.venv/bin/iai-mcp"
|
||||
|
||||
# Check template drift using a python one-liner (avoids shell grep, which is
|
||||
# hook-blocked in this repo's dev env).
|
||||
TEMPLATE_CHECK="$("${VENV_PY}" - <<'PY'
|
||||
import pathlib, sys
|
||||
home = pathlib.Path.home()
|
||||
installed = home / "Library/LaunchAgents/com.iai-mcp.daemon.plist"
|
||||
template = pathlib.Path.cwd() / "deploy/launchd/com.iai-mcp.daemon.plist"
|
||||
if not installed.exists() or not template.exists():
|
||||
print("none"); sys.exit(0)
|
||||
# Substitute USERNAME placeholder and compare env-var + args payload.
|
||||
rendered = template.read_text().replace("{USERNAME}", home.name)
|
||||
a_env = "IAI_MCP_STORE" in installed.read_text() and home.as_posix() + "/.iai-mcp" in installed.read_text()
|
||||
b_env = "IAI_MCP_STORE" in rendered and home.as_posix() + "/.iai-mcp" in rendered
|
||||
print("drift" if a_env != b_env else "same")
|
||||
PY
|
||||
)"
|
||||
|
||||
if [ "${TEMPLATE_CHECK}" = "drift" ]; then
|
||||
warn "launchd plist template drift detected"
|
||||
warn "run: '${IAI_MCP} daemon uninstall --yes && ${IAI_MCP} daemon install --yes' to pick up the new plist"
|
||||
fi
|
||||
|
||||
if "${IAI_MCP}" daemon status >/dev/null 2>&1; then
|
||||
# daemon status exits 0 only when running
|
||||
"${IAI_MCP}" daemon stop >/dev/null 2>&1 || true
|
||||
sleep 2
|
||||
"${IAI_MCP}" daemon start >/dev/null 2>&1 || warn "daemon start returned non-zero; check 'iai-mcp daemon status'"
|
||||
ok "daemon restarted on new code"
|
||||
else
|
||||
ok "daemon not running — nothing to restart"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
step "done"
|
||||
if [ "${NOOP}" = "1" ]; then
|
||||
ok "no-op — everything already current"
|
||||
else
|
||||
ok "updated to $(git rev-parse --short HEAD)"
|
||||
echo
|
||||
git log --oneline "${BEFORE}..${AFTER}"
|
||||
fi
|
||||
Loading…
Add table
Add a link
Reference in a new issue