ai-app: a phone interface to Claude Code and llama.cpp sessions
A Rust backend that owns the sessions and an Android app that reads them. The server spawns and adopts CLI processes, normalises everything they emit into one event model, keeps the transcript, and serves it over pinned TLS on a WireGuard interface; the phone streams that, replies, sends images, and imports conversations the machine already has. `AGENTS.md` is the working guide -- what runs where, what has been measured, and the faults that were expensive to find. `PLAN.md` is the design record. History before this point was squashed away. It was a personal project's running commentary and carried a name and a couple of machine paths that have no business in a public repository; the tree is what mattered and the tree is here.
This commit is contained in:
commit
b172c464ea
100 files changed
+31795
No files matched your search
Executable
+149
@@ -0,0 +1,149 @@
|
||||
#!/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"
|
||||
SETUP=$(api "https://127.0.0.1:$PORT/setups" | 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 "{\"setup\":\"$SETUP\",\"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
|
||||
Reference in new issue
Block a user