150 lines
6.6 KiB
Bash
Executable File
150 lines
6.6 KiB
Bash
Executable File
#!/bin/sh
|
|
# Puts a real Claude Code conversation on the emulator, for looking at the
|
|
# transcript screen under content it was not written against.
|
|
#
|
|
# The echo driver's fixtures (`/mixed`, `/stream`) are the right rig for most
|
|
# things and the wrong one for anything whose cost scales with what was
|
|
# actually written: a real reply is longer, is real markdown, and carries tool
|
|
# calls whose input and output are kilobytes rather than a word. Two faults
|
|
# were invisible until a real transcript was loaded -- a page of history
|
|
# landing mid-fling threw the reader back to the newest end, and parsing one
|
|
# real reply took 51ms against 4.6ms for a synthetic one.
|
|
#
|
|
# ./debug-transcript.sh # newest transcript in ~/.claude/projects
|
|
# ./debug-transcript.sh dev-updater # newest one whose project path matches
|
|
# ./debug-transcript.sh -b dev-updater # the biggest one instead of the newest
|
|
# ./debug-transcript.sh -d 350 # hold every response back 350ms
|
|
#
|
|
# **The transcript never enters the repository.** These files are private --
|
|
# they hold whatever was said, read and written in that session -- so this
|
|
# copies one into /tmp and points an isolated server at it. Nothing it makes
|
|
# is committed, and ~/repos is shared with the host besides.
|
|
#
|
|
# What it builds, all of it disposable:
|
|
# /tmp/ai-app-debug/home a HOME holding only the copied transcript, so
|
|
# the import cannot see or resume a live session
|
|
# /tmp/ai-app-debug/sessions that server's own data directory
|
|
# a server on PORT, with its own config and the real CA (so the installed
|
|
# APK, which pins the CA of the machine that built it, still trusts it)
|
|
set -eu
|
|
|
|
PORT="${PORT:-8455}"
|
|
DELAY=0
|
|
MATCH=""
|
|
BIGGEST=""
|
|
STOP=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
-d|--delay) DELAY="$2"; shift 2 ;;
|
|
-b|--biggest) BIGGEST=yes; shift ;;
|
|
--stop) STOP=yes; shift ;;
|
|
-h|--help) sed -n '2,29p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
*) MATCH="$1"; shift ;;
|
|
esac
|
|
done
|
|
|
|
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
cd "$SCRIPT_DIR"
|
|
REPO=$(dirname "$SCRIPT_DIR")
|
|
WORK=/tmp/ai-app-debug
|
|
PROJECTS="$HOME/.claude/projects"
|
|
|
|
# Whatever the last run left, before this one takes the port again.
|
|
#
|
|
# Importing spawns `claude --resume` so the conversation can be continued, and
|
|
# those outlive the server that started them: twelve accumulated over one
|
|
# afternoon of re-running this. They are found by the scratch HOME and nothing
|
|
# else, because every other `claude` on this machine is somebody's live session
|
|
# -- including the one that may be running this script.
|
|
stop_previous() {
|
|
for pid in $(pgrep -x claude 2>/dev/null); do
|
|
home=$(tr '\0' '\n' <"/proc/$pid/environ" 2>/dev/null | sed -n 's/^HOME=//p')
|
|
if [ "$home" = "$WORK/home" ]; then kill "$pid" 2>/dev/null || true; fi
|
|
done
|
|
pkill -f "[a]i-server --bind 127.0.0.1 --port $PORT" 2>/dev/null || true
|
|
# Gone, not merely signalled: the next start binds the same port.
|
|
while pgrep -f "[a]i-server --bind 127.0.0.1 --port $PORT" >/dev/null 2>&1; do sleep 1; done
|
|
}
|
|
|
|
stop_previous
|
|
if [ -n "$STOP" ]; then
|
|
echo "Stopped the debug server on port $PORT and anything it spawned."
|
|
exit 0
|
|
fi
|
|
|
|
# Newest first, so with no argument you get the conversation you were just in.
|
|
# `--biggest` is the other question worth asking of this directory, and the one
|
|
# a scrolling test wants: the longest conversation on the machine is the one
|
|
# with enough rows to page backwards through, and the newest is routinely a
|
|
# session five minutes old with nothing in it.
|
|
if [ -n "$BIGGEST" ]; then
|
|
SRC=$(ls -S "$PROJECTS"/*"$MATCH"*/*.jsonl 2>/dev/null | head -1)
|
|
else
|
|
SRC=$(ls -t "$PROJECTS"/*"$MATCH"*/*.jsonl 2>/dev/null | head -1)
|
|
fi
|
|
if [ -z "$SRC" ]; then
|
|
echo "No Claude Code transcript under $PROJECTS matching '${MATCH:-anything}'." >&2
|
|
echo "Sessions are written there as <encoded-cwd>/<session-id>.jsonl." >&2
|
|
exit 1
|
|
fi
|
|
ID=$(basename "$SRC" .jsonl)
|
|
PROJECT=$(basename "$(dirname "$SRC")")
|
|
echo "==> Using $PROJECT/$ID ($(wc -l < "$SRC") lines, $(du -h "$SRC" | cut -f1))"
|
|
|
|
# A HOME of its own is the isolation: `import::list` enumerates
|
|
# "$HOME"/.claude/projects/*/*.jsonl through the transport, so a server started
|
|
# with this one can only ever see the copy. That matters for more than tidiness
|
|
# -- importing spawns `claude --resume <id>`, and against the real file that
|
|
# would be a second CLI writing to a conversation somebody may still be in.
|
|
rm -rf "$WORK"
|
|
mkdir -p "$WORK/home/.claude/projects/$PROJECT"
|
|
cp "$SRC" "$WORK/home/.claude/projects/$PROJECT/$ID.jsonl"
|
|
|
|
CERTS="${XDG_CONFIG_HOME:-$HOME/.config}/ai-app/certs"
|
|
if [ ! -f "$CERTS/ca.pem" ]; then
|
|
echo "No CA at $CERTS/ca.pem -- start ai-server once normally first." >&2
|
|
exit 1
|
|
fi
|
|
|
|
SERVER="$REPO/server/target/debug/ai-server"
|
|
[ -x "$SERVER" ] || (cd "$REPO/server" && cargo build)
|
|
|
|
echo "==> Starting server on port $PORT (delay ${DELAY}ms)"
|
|
HOME="$WORK/home" setsid nohup "$SERVER" \
|
|
--bind 127.0.0.1 --port "$PORT" \
|
|
--config "$WORK/config.ron" --data-dir "$WORK/sessions" --certs "$CERTS" \
|
|
--delay "$DELAY" >"$WORK/server.log" 2>&1 </dev/null &
|
|
until grep -q "serving https" "$WORK/server.log" 2>/dev/null; do sleep 1; done
|
|
TOKEN=$(grep -o 'token=[A-Za-z0-9_-]*' "$WORK/server.log" | head -1 | cut -d= -f2)
|
|
|
|
api() { curl -s --cacert "$CERTS/ca.pem" -H "Authorization: Bearer $TOKEN" "$@"; }
|
|
|
|
echo "==> Importing"
|
|
MACHINE=$(api "https://127.0.0.1:$PORT/machines" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p' | head -1)
|
|
SESSION=$(api -H 'Content-Type: application/json' -X POST \
|
|
"https://127.0.0.1:$PORT/sessions" \
|
|
-d "{\"machine\":\"$MACHINE\",\"provider\":\"claude-cli\",\"title\":\"$PROJECT\",\"import\":\"$ID\"}" \
|
|
| sed -n 's/.*"id":"\([^"]*\)".*/\1/p' | head -1)
|
|
echo " session $SESSION, $(wc -l < "$WORK/sessions/$SESSION/transcript.jsonl") events"
|
|
|
|
# 10.0.2.2 is the emulator's route to this VM's loopback. The `&` are quoted
|
|
# on the *device* side: adb runs its argument through a shell there, which
|
|
# would otherwise cut the URI at the first one and enrol with no token.
|
|
if command -v adb >/dev/null 2>&1 && [ -n "$(adb devices | awk '$2=="device"{print $1}')" ]; then
|
|
echo "==> Enrolling the app"
|
|
adb shell "am start -a android.intent.action.VIEW \
|
|
-d 'aiapp://enroll?host=10.0.2.2&port=$PORT&token=$TOKEN'" >/dev/null
|
|
fi
|
|
|
|
cat <<EOF
|
|
|
|
Ready. Open "$PROJECT" in the app; press Refresh if the list is stale.
|
|
|
|
token $TOKEN
|
|
log $WORK/server.log
|
|
stop $SCRIPT_DIR/debug-transcript.sh --stop
|
|
|
|
The imported session is a claude-cli one, so **do not send it a message**:
|
|
that continues a real conversation on a real account. It is for reading.
|
|
EOF
|