A hook resumed every finished run on a timer — unasked-for, and expensive: a `--resume` never hits the finished run's prompt cache, so each firing re-created the whole transcript as fresh input tokens. The prompts stay, the scheduler goes. A CTA is a prompt file plus a button under the last message of a finished conversation: click loads its prompt into the resume box (editable before it is sent), long-press fires it at the session in the background. The same prompts are selectable as `cta:<id>` tags in every composer. The seeded `complete` CTA also does the post-processing the hook never could afford: `conv-scaffold` reduces the conversation to markdown — metadata, its prompts, merged bash/tool/subagent summaries, one squashed diff across every commit it produced — and one subagent fills in the Summary/Analysis and publishes it onto `meta.summary`, rendered under the thread. - backend: ctas.py (store + seeded Complete), scaffold.py (+ /scaffold, PUT /summary), /api/ctas CRUD/run/prompt/reorder; hooks.py and its scheduler, pending queue and routes deleted (read-only /api/claude-hooks stays) - frontend: ConversationCtas section, Settings → CTAs, cta tags, CtaBadge - scripts/conv-scaffold.sh: scaffold + publish CLI Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
136 lines
5.0 KiB
Bash
Executable File
136 lines
5.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# conv-scaffold — scaffold a conversation's completion summary, and publish it.
|
|
#
|
|
# (In the homelab this is on PATH as `conv-scaffold`, via the alias shim at
|
|
# services/ai-agent/cli/conv-scaffold.sh which execs this script.)
|
|
#
|
|
# The CLI half of the `Complete` CTA (see backend/scaffold.py). The backend
|
|
# already parses transcripts and aggregates a conversation's commits, so the
|
|
# expensive, mechanical part of "summarise what happened here" — metadata, the
|
|
# prompts, the merged bash/tool/subagent summaries, one squashed diff across
|
|
# every commit — is generated instead of re-read out of the transcript by a
|
|
# model. What comes back is markdown with two sections left blank.
|
|
#
|
|
# conv-scaffold # scaffold THIS conversation → prints the path
|
|
# conv-scaffold --session <sid> # …a specific session
|
|
# conv-scaffold --stdout # write to stdout instead of a file
|
|
# conv-scaffold publish <file> # publish a filled-in scaffold as the summary
|
|
# conv-scaffold show # print the currently published summary
|
|
#
|
|
# Scaffolds are written to $TMPDIR/conv-scaffold-<sid>.md by default; pass
|
|
# `-o <path>` to choose. Publishing stores the markdown on the conversation's
|
|
# metadata sidecar, which is what the viewer renders under the thread — an
|
|
# untouched scaffold is refused (HTTP 422), because a report of nothing is worse
|
|
# than no report.
|
|
#
|
|
# Session resolution, in order: --session, $CLAUDE_SESSION_ID, then the homelab
|
|
# conv-meta helper (PID-ancestry match) when it is on this machine.
|
|
set -uo pipefail
|
|
|
|
AI_AGENT_URL="${AI_AGENT_URL:-http://127.0.0.1:8096}"
|
|
|
|
die() { echo "conv-scaffold: $*" >&2; exit 1; }
|
|
|
|
resolve_session() {
|
|
[[ -n "${SESSION:-}" ]] && { echo "$SESSION"; return; }
|
|
[[ -n "${CLAUDE_SESSION_ID:-}" ]] && { echo "$CLAUDE_SESSION_ID"; return; }
|
|
local helper="$HOME/homelab/.claude/skills/conv-meta/scripts/resolve-session.sh"
|
|
[[ -x "$helper" ]] && "$helper" 2>/dev/null && return
|
|
echo ""
|
|
}
|
|
|
|
# sessionId → the transcript id the API is keyed by (`<slug>/<sid>.jsonl`).
|
|
conv_id_for() {
|
|
local sid="$1"
|
|
curl -s -m 20 "$AI_AGENT_URL/api/conversations" \
|
|
| python3 -c '
|
|
import json, sys
|
|
sid = sys.argv[1]
|
|
try:
|
|
data = json.load(sys.stdin)
|
|
except ValueError:
|
|
sys.exit(1)
|
|
for c in data.get("conversations") or []:
|
|
if c.get("sessionId") == sid:
|
|
print(c.get("id") or "")
|
|
break
|
|
' "$sid"
|
|
}
|
|
|
|
cmd_scaffold() {
|
|
local sid out
|
|
sid="$(resolve_session)"
|
|
[[ -n "$sid" ]] || die "could not work out which session this is — pass --session <id>"
|
|
local cid; cid="$(conv_id_for "$sid")"
|
|
[[ -n "$cid" ]] || die "no transcript found for session $sid (has it synced yet?)"
|
|
local body code tmp
|
|
tmp="$(mktemp)"
|
|
code="$(curl -s -m 120 -o "$tmp" -w '%{http_code}' \
|
|
"$AI_AGENT_URL/api/conversations/$cid/scaffold")"
|
|
if [[ "$code" != "200" ]]; then
|
|
body="$(cat "$tmp")"; rm -f "$tmp"
|
|
die "scaffold failed (HTTP $code): ${body:0:300}"
|
|
fi
|
|
if [[ -n "${TO_STDOUT:-}" ]]; then
|
|
cat "$tmp"; rm -f "$tmp"; return
|
|
fi
|
|
out="${OUTFILE:-${TMPDIR:-/tmp}/conv-scaffold-$sid.md}"
|
|
mv "$tmp" "$out"
|
|
echo "$out"
|
|
echo "next: fill in the ## Summary and ## Analysis sections, then run" >&2
|
|
echo " conv-scaffold publish $out" >&2
|
|
}
|
|
|
|
cmd_publish() {
|
|
local file="${1:-}"
|
|
[[ -n "$file" && -f "$file" ]] || die "usage: conv-scaffold publish <file>"
|
|
local sid; sid="$(resolve_session)"
|
|
[[ -n "$sid" ]] || die "could not work out which session this is — pass --session <id>"
|
|
local cid; cid="$(conv_id_for "$sid")"
|
|
[[ -n "$cid" ]] || die "no transcript found for session $sid"
|
|
local payload tmp code
|
|
payload="$(python3 -c 'import json,sys; print(json.dumps({"markdown": open(sys.argv[1], encoding="utf-8").read()}))' "$file")"
|
|
tmp="$(mktemp)"
|
|
code="$(curl -s -m 30 -o "$tmp" -w '%{http_code}' -X PUT \
|
|
-H 'Content-Type: application/json' -d "$payload" \
|
|
"$AI_AGENT_URL/api/conversations/$cid/summary")"
|
|
if [[ "$code" != "200" ]]; then
|
|
local body; body="$(cat "$tmp")"; rm -f "$tmp"
|
|
die "publish failed (HTTP $code): ${body:0:300}"
|
|
fi
|
|
rm -f "$tmp"
|
|
echo "published — the summary now shows under the conversation"
|
|
}
|
|
|
|
cmd_show() {
|
|
local sid; sid="$(resolve_session)"
|
|
[[ -n "$sid" ]] || die "could not work out which session this is — pass --session <id>"
|
|
curl -s -m 20 "$AI_AGENT_URL/api/conversation-meta" \
|
|
| python3 -c '
|
|
import json, sys
|
|
sid = sys.argv[1]
|
|
meta = (json.load(sys.stdin) or {}).get("meta") or {}
|
|
entry = (meta.get(sid) or {}).get("summary") or {}
|
|
print(entry.get("markdown") or "(no summary published for this conversation)")
|
|
' "$sid"
|
|
}
|
|
|
|
ACTION=scaffold
|
|
ARGS=()
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
publish|show) ACTION="$1"; shift ;;
|
|
--session) SESSION="${2:-}"; shift 2 ;;
|
|
-o|--out) OUTFILE="${2:-}"; shift 2 ;;
|
|
--stdout) TO_STDOUT=1; shift ;;
|
|
-h|--help) sed -n '2,27p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
*) ARGS+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
case "$ACTION" in
|
|
scaffold) cmd_scaffold ;;
|
|
publish) cmd_publish "${ARGS[0]:-}" ;;
|
|
show) cmd_show ;;
|
|
esac
|