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
140
deploy/hooks/iai-mcp-session-capture.sh
Executable file
140
deploy/hooks/iai-mcp-session-capture.sh
Executable file
|
|
@ -0,0 +1,140 @@
|
|||
#!/usr/bin/env bash
|
||||
# IAI-MCP Stop hook — ambient WRITE-side capture (Plan 06 + Phase 7.1).
|
||||
#
|
||||
# Fires when a Claude Code session ends. Reads the session's JSONL transcript,
|
||||
# batch-captures user + assistant turns into the iai-mcp episodic tier through
|
||||
# `iai-mcp capture-transcript --no-spawn`. NEVER spawns a daemon (Phase 7.1 R3).
|
||||
# If the daemon is unreachable, the call defers events to
|
||||
# ~/.iai-mcp/.deferred-captures/ for the daemon to drain on next socket
|
||||
# activation (handled by drain_deferred_captures in daemon.main + _tick_body
|
||||
# WAKE handler — Plan 07.1-06).
|
||||
#
|
||||
# Fail-safe by design: any error exits 0 so session teardown is never blocked.
|
||||
# Logs go to ~/.iai-mcp/logs/capture-YYYY-MM-DD.log for audit.
|
||||
#
|
||||
# Hook payload (stdin JSON from Claude Code) contains:
|
||||
# - session_id (UUID of the session that just ended)
|
||||
# - transcript_path (absolute path to the session JSONL) — available in
|
||||
# newer Claude Code builds; we fall back to scanning the
|
||||
# per-project transcript dir for the matching session_id.
|
||||
# - cwd (working directory at session end)
|
||||
|
||||
set -u # no -e: we must not abort on errors, fail-safe is paramount
|
||||
input=$(cat 2>/dev/null || true)
|
||||
|
||||
# Best-effort jq; fall back to Python if jq missing.
|
||||
extract() {
|
||||
local key=$1
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
printf '%s' "$input" | jq -r ".${key} // empty" 2>/dev/null
|
||||
else
|
||||
printf '%s' "$input" | /usr/bin/python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
print(d.get('${key}', '') or '')
|
||||
except Exception:
|
||||
print('')
|
||||
" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
session_id=$(extract "session_id")
|
||||
transcript_path=$(extract "transcript_path")
|
||||
cwd=$(extract "cwd")
|
||||
|
||||
# Fallback: locate transcript if the hook payload didn't include its path.
|
||||
# Claude Code stores transcripts under ~/.claude/projects/{cwd-hash}/{uuid}.jsonl
|
||||
if [[ -z "$transcript_path" && -n "$session_id" ]]; then
|
||||
projects_dir="$HOME/.claude/projects"
|
||||
if [[ -d "$projects_dir" ]]; then
|
||||
# Look for the most recent file whose basename starts with session_id.
|
||||
# ls -t (mtime newest first). Avoid `find` per the project's no-grep hook.
|
||||
for d in "$projects_dir"/*/; do
|
||||
candidate="${d}${session_id}.jsonl"
|
||||
if [[ -f "$candidate" ]]; then
|
||||
transcript_path="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$HOME/.iai-mcp/logs" 2>/dev/null || true
|
||||
log="$HOME/.iai-mcp/logs/capture-$(date -u +%Y-%m-%d).log"
|
||||
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
{
|
||||
echo "---"
|
||||
echo "$ts session=$session_id cwd=$cwd transcript=$transcript_path"
|
||||
} >> "$log" 2>/dev/null
|
||||
|
||||
# Skip if we couldn't find anything to capture.
|
||||
if [[ -z "$transcript_path" || ! -f "$transcript_path" ]]; then
|
||||
echo "$ts skipped: no transcript found" >> "$log" 2>/dev/null
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Locate the project's venv-installed `iai-mcp` CLI. Cache the last-known-good
|
||||
# path in ~/.iai-mcp/.cli-path to avoid re-scanning on every session end.
|
||||
cli_cache="$HOME/.iai-mcp/.cli-path"
|
||||
iai_cli=""
|
||||
if [[ -f "$cli_cache" ]]; then
|
||||
cached=$(cat "$cli_cache" 2>/dev/null || true)
|
||||
[[ -x "$cached" ]] && iai_cli="$cached"
|
||||
fi
|
||||
if [[ -z "$iai_cli" ]]; then
|
||||
# Resolve via PATH first (covers ~/.local/bin/iai-mcp installed by scripts/install.sh)
|
||||
path_cli="$(command -v iai-mcp 2>/dev/null || true)"
|
||||
if [[ -n "$path_cli" && -x "$path_cli" ]]; then
|
||||
iai_cli="$path_cli"
|
||||
else
|
||||
# Fall back to common clone locations
|
||||
for candidate in \
|
||||
"$HOME/.local/bin/iai-mcp" \
|
||||
"$HOME/iai-mcp/.venv/bin/iai-mcp" \
|
||||
"$HOME/IAI-MCP/.venv/bin/iai-mcp" \
|
||||
"/usr/local/bin/iai-mcp" \
|
||||
"/opt/homebrew/bin/iai-mcp"; do
|
||||
if [[ -x "$candidate" ]]; then
|
||||
iai_cli="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [[ -n "$iai_cli" ]]; then
|
||||
printf '%s' "$iai_cli" > "$cli_cache" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$iai_cli" ]]; then
|
||||
echo "$ts skipped: iai-mcp CLI not found" >> "$log" 2>/dev/null
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run capture with a 30s hard timeout — if it hangs, the session must still
|
||||
# end cleanly. `timeout` is in coreutils (macOS: brew install coreutils). We
|
||||
# fall back to a background kill loop if absent.
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
result=$(timeout 30 "$iai_cli" capture-transcript --no-spawn \
|
||||
--session-id "$session_id" \
|
||||
--max-turns 200 \
|
||||
"$transcript_path" 2>&1)
|
||||
elif command -v gtimeout >/dev/null 2>&1; then
|
||||
result=$(gtimeout 30 "$iai_cli" capture-transcript --no-spawn \
|
||||
--session-id "$session_id" \
|
||||
--max-turns 200 \
|
||||
"$transcript_path" 2>&1)
|
||||
else
|
||||
result=$("$iai_cli" capture-transcript --no-spawn \
|
||||
--session-id "$session_id" \
|
||||
--max-turns 200 \
|
||||
"$transcript_path" 2>&1)
|
||||
fi
|
||||
rc=$?
|
||||
|
||||
{
|
||||
echo "$ts rc=$rc result=$result"
|
||||
} >> "$log" 2>/dev/null
|
||||
|
||||
exit 0
|
||||
83
deploy/launchd/com.iai-mcp.daemon.plist
Normal file
83
deploy/launchd/com.iai-mcp.daemon.plist
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?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">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.iai-mcp.daemon</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/local/bin/python3</string>
|
||||
<string>-m</string>
|
||||
<string>iai_mcp.daemon</string>
|
||||
</array>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
<!--
|
||||
Phase 10.6 Plan 10.6-01 Task 1.7: KeepAlive policy uses ONLY
|
||||
`Crashed=true`. With this policy, launchd respawns ONLY on
|
||||
a non-zero exit (the new lifecycle state machine exits 0
|
||||
gracefully on Hibernation; an MCP wrapper kickstart is the
|
||||
sole wake mechanism in steady state).
|
||||
|
||||
Removed (was Phase 07.8): `SuccessfulExit=false`, which paired
|
||||
with the legacy 75/0 exit-code branching. Now that exit code
|
||||
is uniformly 0 for graceful shutdown, `SuccessfulExit=false`
|
||||
would put us in a respawn loop.
|
||||
-->
|
||||
<key>KeepAlive</key>
|
||||
<dict>
|
||||
<key>Crashed</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
<key>ThrottleInterval</key>
|
||||
<integer>5</integer>
|
||||
|
||||
<key>ProcessType</key>
|
||||
<string>Background</string>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/{USERNAME}/Library/Logs/iai-mcp-daemon.stdout.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/{USERNAME}/Library/Logs/iai-mcp-daemon.stderr.log</string>
|
||||
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/Users/{USERNAME}</string>
|
||||
|
||||
<!--
|
||||
Phase 10.6 Plan 10.6-01 Task 1.7: env-var update.
|
||||
REMOVED:
|
||||
- IAI_MCP_RSS_RESTART_THRESHOLD_MB (legacy RSS-watchdog gone)
|
||||
- IAI_DAEMON_IDLE_SHUTDOWN_SECS (legacy socket idle_watcher gone)
|
||||
- IAI_MCP_SKIP_STARTUP_OPTIMIZE (legacy boot-time optimize defer gone)
|
||||
ADDED (lifecycle cadence + sleep quarantine):
|
||||
- LIFECYCLE_DROWSY_AFTER_SEC (default 300 == 5 min)
|
||||
- LIFECYCLE_SLEEP_HEARTBEAT_IDLE_SEC (default 1800 == 30 min)
|
||||
- LIFECYCLE_HIBERNATE_AFTER_SEC (default 7200 == 2 h)
|
||||
- IAI_MCP_SLEEP_QUARANTINE_TTL_HOURS (default 24)
|
||||
-->
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/usr/local/bin:/usr/bin:/bin</string>
|
||||
<key>IAI_MCP_STORE</key>
|
||||
<string>/Users/{USERNAME}/.iai-mcp</string>
|
||||
<key>HOME</key>
|
||||
<string>/Users/{USERNAME}</string>
|
||||
<key>LANG</key>
|
||||
<string>en_US.UTF-8</string>
|
||||
<key>LIFECYCLE_DROWSY_AFTER_SEC</key>
|
||||
<string>300</string>
|
||||
<key>LIFECYCLE_SLEEP_HEARTBEAT_IDLE_SEC</key>
|
||||
<string>1800</string>
|
||||
<key>LIFECYCLE_HIBERNATE_AFTER_SEC</key>
|
||||
<string>7200</string>
|
||||
<key>IAI_MCP_SLEEP_QUARANTINE_TTL_HOURS</key>
|
||||
<string>24</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
39
deploy/systemd/iai-mcp-daemon.service
Normal file
39
deploy/systemd/iai-mcp-daemon.service
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# IAI-MCP Sleep Daemon -- systemd user unit (Plan 04-01, DAEMON-01)
|
||||
#
|
||||
# Install at ~/.config/systemd/user/iai-mcp-daemon.service, then:
|
||||
# systemctl --user daemon-reload
|
||||
# systemctl --user enable --now iai-mcp-daemon.service
|
||||
#
|
||||
# For survival past logout (headless servers):
|
||||
# loginctl enable-linger $USER
|
||||
#
|
||||
# C3 / guard: NO paid-API env var in Environment= lines. host_cli.py
|
||||
# scrubs the subprocess env at spawn time; the unit env is intentionally minimal.
|
||||
|
||||
[Unit]
|
||||
Description=IAI-MCP Sleep Daemon -- autonomous neural consolidation between sessions
|
||||
After=default.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/python3 -m iai_mcp.daemon
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
StartLimitIntervalSec=60
|
||||
StartLimitBurst=3
|
||||
|
||||
Environment="IAI_MCP_STORE=%h/.iai-mcp"
|
||||
Environment="LANG=en_US.UTF-8"
|
||||
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=iai-mcp-daemon
|
||||
|
||||
# Graceful shutdown: systemd default TimeoutStopSec is 90s; we tighten to 60s
|
||||
# so stop never kills us mid-Claude (subprocess timeout is 120s but the
|
||||
# daemon aborts the pending call cleanly on SIGTERM).
|
||||
TimeoutStopSec=60
|
||||
KillSignal=SIGTERM
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
Loading…
Add table
Add a link
Reference in a new issue