Completing a conversation is now a skill the session runs on itself before it ends, instead of a button that resumes it afterwards. A resume is a new process, so it re-creates the whole transcript as fresh input tokens — the same tidy-up costs several times more once the process has exited. The spawn guidelines point the session at `.claude/skills/complete/SKILL.md` after its final notification; that pass waits 30s (a window to redirect after reading the result), publishes the summary via conv-scaffold + one subagent, tidies up, and signs off with COMPLETED. With the prompts living in a skill, the whole CTA layer goes: - backend: ctas.py, /api/ctas* (+ /run, /prompt, /reorder), the seeded prompt writer, the Cta* schemas, meta.ctas and its merge path. /api/claude-hooks stays — it just no longer lives in a CTA-shaped section. - frontend: Settings -> CTAs page, the CTA buttons under a finished thread, the CTA badge, ctaIcons, the cta:<id> composer tags, and the ctas SSE event. - the native Claude Code hooks list moves onto the main Settings page (/settings#hooks), where it is the only hooks surface left. The COMPLETED seal now reads `meta.completedAt`, derived from the transcript's COMPLETED marker the parser already flags, rather than the meta.ctas["complete"] ledger — nothing has to stamp it. What survives of the old pass is unchanged: conv-scaffold, the published summary on meta.summary, and the Summary card (ConversationCtas -> ConversationSummary). Existing meta.ctas data is left alone in the store; it is simply no longer read. 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` skill (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
|