uv-suite 0.26.1 → 0.26.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,124 @@
1
+ #!/bin/bash
2
+ # UV Suite helper: locate per-session checkpoint paths and print metadata.
3
+ # Used by the /checkpoint and /restore slash commands.
4
+ #
5
+ # Usage:
6
+ # checkpoint-helper.sh dir # ensure + print the dir for current session
7
+ # checkpoint-helper.sh meta # print session metadata as shell-eval'able lines
8
+ # checkpoint-helper.sh frontmatter # YAML frontmatter to embed at the top of a checkpoint
9
+ # checkpoint-helper.sh latest # cat the latest checkpoint for current session (with fallback)
10
+ # checkpoint-helper.sh list # list all sessions that have checkpoints, newest first
11
+
12
+ resolve_paths() {
13
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
14
+ STATE_DIR="$PROJECT_DIR/.uv-suite-state"
15
+ SID="${UVS_SESSION_ID:-}"
16
+ if [ -z "$SID" ] && [ -f "$STATE_DIR/current-session.txt" ]; then
17
+ SID=$(cat "$STATE_DIR/current-session.txt" 2>/dev/null)
18
+ fi
19
+ CHECKPOINTS_ROOT="$PROJECT_DIR/uv-out/checkpoints"
20
+ SESSION_CP_DIR=""
21
+ [ -n "$SID" ] && SESSION_CP_DIR="$CHECKPOINTS_ROOT/$SID"
22
+ META_FILE=""
23
+ [ -n "$SID" ] && META_FILE="$STATE_DIR/sessions/$SID.json"
24
+ }
25
+
26
+ print_meta_field() {
27
+ # $1 = field name; reads from $META_FILE; empty if missing
28
+ [ -z "$META_FILE" ] || [ ! -f "$META_FILE" ] && { echo ""; return; }
29
+ if command -v jq >/dev/null 2>&1; then
30
+ jq -r --arg k "$1" '.[$k] // ""' "$META_FILE" 2>/dev/null
31
+ else
32
+ grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$META_FILE" | head -1 | sed "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\(.*\)\"/\1/"
33
+ fi
34
+ }
35
+
36
+ resolve_paths
37
+
38
+ case "$1" in
39
+ dir)
40
+ if [ -n "$SESSION_CP_DIR" ]; then
41
+ mkdir -p "$SESSION_CP_DIR"
42
+ echo "$SESSION_CP_DIR"
43
+ else
44
+ mkdir -p "$CHECKPOINTS_ROOT"
45
+ echo "$CHECKPOINTS_ROOT"
46
+ fi
47
+ ;;
48
+ meta)
49
+ echo "uvs_session_id=${SID:-}"
50
+ echo "session_name=$(print_meta_field name)"
51
+ echo "session_kind=$(print_meta_field kind)"
52
+ echo "session_purpose=$(print_meta_field purpose)"
53
+ echo "session_priority=$(print_meta_field priority)"
54
+ echo "persona=$(print_meta_field persona)"
55
+ ;;
56
+ frontmatter)
57
+ NAME=$(print_meta_field name)
58
+ KIND=$(print_meta_field kind)
59
+ PURPOSE=$(print_meta_field purpose)
60
+ PRIORITY=$(print_meta_field priority)
61
+ PERSONA=$(print_meta_field persona)
62
+ NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
63
+ cat <<EOF
64
+ ---
65
+ uvs_session_id: ${SID:-}
66
+ session_name: ${NAME}
67
+ session_kind: ${KIND}
68
+ session_purpose: ${PURPOSE}
69
+ session_priority: ${PRIORITY}
70
+ persona: ${PERSONA}
71
+ checkpoint_at: ${NOW}
72
+ ---
73
+ EOF
74
+ ;;
75
+ latest)
76
+ if [ -n "$SESSION_CP_DIR" ] && [ -f "$SESSION_CP_DIR/latest.md" ]; then
77
+ cat "$SESSION_CP_DIR/latest.md"
78
+ elif [ -f "$CHECKPOINTS_ROOT/latest.md" ]; then
79
+ echo "(no per-session checkpoint for ${SID:-this session}; showing legacy global latest.md)"
80
+ echo
81
+ cat "$CHECKPOINTS_ROOT/latest.md"
82
+ else
83
+ echo "No checkpoint found at $CHECKPOINTS_ROOT. Run /checkpoint to create one."
84
+ fi
85
+ ;;
86
+ list)
87
+ [ ! -d "$CHECKPOINTS_ROOT" ] && { echo "No checkpoints directory at $CHECKPOINTS_ROOT"; exit 0; }
88
+ found=0
89
+ for d in "$CHECKPOINTS_ROOT"/*/; do
90
+ [ -d "$d" ] || continue
91
+ cp_sid=$(basename "$d")
92
+ cp_meta="$STATE_DIR/sessions/$cp_sid.json"
93
+ cp_name=""
94
+ cp_priority=""
95
+ if [ -f "$cp_meta" ]; then
96
+ if command -v jq >/dev/null 2>&1; then
97
+ cp_name=$(jq -r '.name // ""' "$cp_meta" 2>/dev/null)
98
+ cp_priority=$(jq -r '.priority // ""' "$cp_meta" 2>/dev/null)
99
+ else
100
+ cp_name=$(grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' "$cp_meta" | head -1 | sed 's/.*"name"[[:space:]]*:[[:space:]]*"\(.*\)"/\1/')
101
+ fi
102
+ fi
103
+ latest=$(ls -t "$d"*.md 2>/dev/null | head -1)
104
+ [ -z "$latest" ] && continue
105
+ ts=$(stat -f '%Sm' -t '%Y-%m-%d %H:%M' "$latest" 2>/dev/null || stat -c '%y' "$latest" 2>/dev/null | cut -c1-16)
106
+ label="${cp_name:-(unlabeled)}"
107
+ [ -n "$cp_priority" ] && label="$label [p:$cp_priority]"
108
+ mark=" "
109
+ [ "$cp_sid" = "$SID" ] && mark="*"
110
+ echo "$mark ${cp_sid:0:8} $ts $label"
111
+ found=1
112
+ done
113
+ [ "$found" -eq 0 ] && echo "No per-session checkpoints yet (current session: ${SID:-none})"
114
+ # Note legacy global checkpoint if present
115
+ if [ -f "$CHECKPOINTS_ROOT/latest.md" ]; then
116
+ ts=$(stat -f '%Sm' -t '%Y-%m-%d %H:%M' "$CHECKPOINTS_ROOT/latest.md" 2>/dev/null || stat -c '%y' "$CHECKPOINTS_ROOT/latest.md" 2>/dev/null | cut -c1-16)
117
+ echo " legacy $ts (pre-metadata global latest.md)"
118
+ fi
119
+ ;;
120
+ *)
121
+ echo "Usage: checkpoint-helper.sh [dir|meta|frontmatter|latest|list]"
122
+ exit 1
123
+ ;;
124
+ esac
@@ -0,0 +1,51 @@
1
+ #!/bin/bash
2
+ # UV Suite Hook: Reframe-and-confirm long user prompts before acting.
3
+ #
4
+ # Reads state from $CLAUDE_PROJECT_DIR/.uv-suite-state/. When mode is "on" and
5
+ # the user's prompt exceeds the configured word count, injects a system-context
6
+ # instruction that tells Claude to restate the request and wait for confirmation
7
+ # before doing any work.
8
+ #
9
+ # State files (toggled by the /confirm slash command):
10
+ # confirm-mode.txt — "on" or "off" (default: on)
11
+ # confirm-threshold.txt — integer (default: 50)
12
+ #
13
+ # Slash commands (lines starting with "/") are always skipped so the toggle
14
+ # itself can run without being intercepted.
15
+
16
+ INPUT=$(cat)
17
+ STATE_DIR="${CLAUDE_PROJECT_DIR:-.}/.uv-suite-state"
18
+
19
+ MODE=$(cat "$STATE_DIR/confirm-mode.txt" 2>/dev/null)
20
+ [ -z "$MODE" ] && MODE="on"
21
+
22
+ THRESHOLD=$(cat "$STATE_DIR/confirm-threshold.txt" 2>/dev/null)
23
+ [ -z "$THRESHOLD" ] && THRESHOLD=50
24
+
25
+ [ "$MODE" = "off" ] && exit 0
26
+
27
+ if command -v jq >/dev/null 2>&1; then
28
+ PROMPT=$(echo "$INPUT" | jq -r '.prompt // empty' 2>/dev/null)
29
+ else
30
+ PROMPT=$(echo "$INPUT" | grep -o '"prompt":"[^"]*"' | head -1 | cut -d'"' -f4)
31
+ fi
32
+
33
+ [ -z "$PROMPT" ] && exit 0
34
+
35
+ case "$PROMPT" in
36
+ /*) exit 0 ;;
37
+ esac
38
+
39
+ WORDS=$(echo "$PROMPT" | wc -w | tr -d ' ')
40
+ [ "$WORDS" -le "$THRESHOLD" ] && exit 0
41
+
42
+ # Emit Claude Code hook output. additionalContext is appended to the system
43
+ # context for this turn. Keep it terse — long instructions get tuned out.
44
+ ADDITIONAL=$(printf '[uv-suite confirm-mode] The user prompt is %s words (threshold %s). Before doing any work or making tool calls, restate what you understood in 1-2 plain sentences and ask the user to confirm. Only proceed once they confirm. The user can disable this with /confirm off or change the threshold with /confirm <number>.' "$WORDS" "$THRESHOLD")
45
+
46
+ if command -v jq >/dev/null 2>&1; then
47
+ jq -nc --arg ctx "$ADDITIONAL" '{hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:$ctx}}'
48
+ else
49
+ ESCAPED=$(printf '%s' "$ADDITIONAL" | sed 's/\\/\\\\/g; s/"/\\"/g')
50
+ printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"%s"}}' "$ESCAPED"
51
+ fi
@@ -0,0 +1,63 @@
1
+ #!/bin/bash
2
+ # UV Suite Hook: Nag user to label session if metadata.name is empty.
3
+ # Event: UserPromptSubmit
4
+ #
5
+ # Reads the metadata file at .uv-suite-state/sessions/$UVS_SESSION_ID.json.
6
+ # If the session has no name, injects an additionalContext nudge once every
7
+ # Nth user prompt (default 3) so Claude reminds the user to run /session-init.
8
+ # Skips when the prompt is itself a slash command.
9
+
10
+ INPUT=$(cat)
11
+ STATE_DIR="${CLAUDE_PROJECT_DIR:-.}/.uv-suite-state"
12
+ SESSIONS_DIR="$STATE_DIR/sessions"
13
+
14
+ # Resolve session id: env first, then current-session pointer
15
+ SID="${UVS_SESSION_ID:-}"
16
+ if [ -z "$SID" ] && [ -f "$STATE_DIR/current-session.txt" ]; then
17
+ SID=$(cat "$STATE_DIR/current-session.txt" 2>/dev/null)
18
+ fi
19
+
20
+ [ -z "$SID" ] && exit 0
21
+ META_FILE="$SESSIONS_DIR/$SID.json"
22
+ [ ! -f "$META_FILE" ] && exit 0
23
+
24
+ # Skip when the session already has a name
25
+ if command -v jq >/dev/null 2>&1; then
26
+ NAME=$(jq -r '.name // ""' "$META_FILE" 2>/dev/null)
27
+ else
28
+ NAME=$(grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' "$META_FILE" | head -1 | sed 's/.*"name"[[:space:]]*:[[:space:]]*"\(.*\)"/\1/')
29
+ fi
30
+ [ -n "$NAME" ] && exit 0
31
+
32
+ # Skip when the user prompt is itself a slash command
33
+ if command -v jq >/dev/null 2>&1; then
34
+ PROMPT=$(echo "$INPUT" | jq -r '.prompt // empty' 2>/dev/null)
35
+ else
36
+ PROMPT=$(echo "$INPUT" | grep -o '"prompt":"[^"]*"' | head -1 | cut -d'"' -f4)
37
+ fi
38
+ case "$PROMPT" in
39
+ /*) exit 0 ;;
40
+ esac
41
+
42
+ # Rate-limit: nag every Nth prompt (default 3)
43
+ INTERVAL="${UVS_LABEL_NAG_INTERVAL:-3}"
44
+ COUNT_FILE="$SESSIONS_DIR/$SID.prompt-count"
45
+ COUNT=$(cat "$COUNT_FILE" 2>/dev/null)
46
+ [ -z "$COUNT" ] && COUNT=0
47
+ COUNT=$((COUNT + 1))
48
+ echo "$COUNT" > "$COUNT_FILE"
49
+
50
+ # Nag on the 1st prompt and every Nth after
51
+ REMAINDER=$((COUNT % INTERVAL))
52
+ if [ "$COUNT" -ne 1 ] && [ "$REMAINDER" -ne 1 ]; then
53
+ exit 0
54
+ fi
55
+
56
+ MSG="[uv-suite] This session has no name yet. Briefly remind the user to run /session-init to set name, kind (long-running/outcome), purpose, and priority — these label the session in the watchtower dashboard. One sentence is enough; then proceed with the user's request."
57
+
58
+ if command -v jq >/dev/null 2>&1; then
59
+ jq -nc --arg ctx "$MSG" '{hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:$ctx}}'
60
+ else
61
+ ESCAPED=$(printf '%s' "$MSG" | sed 's/\\/\\\\/g; s/"/\\"/g')
62
+ printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"%s"}}' "$ESCAPED"
63
+ fi
@@ -0,0 +1,121 @@
1
+ #!/bin/bash
2
+ # UV Suite helper: read or write session metadata.
3
+ # Used by the /session-init slash command.
4
+ #
5
+ # Usage:
6
+ # session-meta.sh show
7
+ # session-meta.sh clear
8
+ # session-meta.sh set-name <free text...>
9
+ # session-meta.sh set-kind long|outcome
10
+ # session-meta.sh set-purpose <free text...>
11
+ # session-meta.sh set-priority low|med|high
12
+ #
13
+ # Reads/writes .uv-suite-state/sessions/$UVS_SESSION_ID.json under
14
+ # $CLAUDE_PROJECT_DIR (falls back to current-session.txt pointer or a new
15
+ # ad-hoc session if neither is available).
16
+
17
+ STATE_DIR="${CLAUDE_PROJECT_DIR:-.}/.uv-suite-state"
18
+ SESSIONS_DIR="$STATE_DIR/sessions"
19
+ mkdir -p "$SESSIONS_DIR"
20
+
21
+ SID="${UVS_SESSION_ID:-}"
22
+ if [ -z "$SID" ] && [ -f "$STATE_DIR/current-session.txt" ]; then
23
+ SID=$(cat "$STATE_DIR/current-session.txt" 2>/dev/null)
24
+ fi
25
+ if [ -z "$SID" ]; then
26
+ SID="ad-hoc-$(date +%s)"
27
+ echo "$SID" > "$STATE_DIR/current-session.txt"
28
+ fi
29
+
30
+ META="$SESSIONS_DIR/$SID.json"
31
+ if [ ! -f "$META" ]; then
32
+ CWD_VAL="${CLAUDE_PROJECT_DIR:-$(pwd)}" SID_VAL="$SID" STARTED="$(date +%s)" python3 -c '
33
+ import json, os
34
+ print(json.dumps({
35
+ "uvs_session_id": os.environ["SID_VAL"],
36
+ "name": "", "kind": "", "purpose": "", "priority": "", "persona": "",
37
+ "cwd": os.environ["CWD_VAL"],
38
+ "started_at": int(os.environ["STARTED"]),
39
+ }, indent=2))
40
+ ' > "$META"
41
+ fi
42
+
43
+ ACTION="$1"
44
+ shift || true
45
+
46
+ print_meta() {
47
+ META_PATH="$META" python3 - <<'PY'
48
+ import json, os
49
+ d = json.load(open(os.environ["META_PATH"]))
50
+ sid = d.get("uvs_session_id", "")[:8]
51
+ name = d.get("name", "") or "(unset)"
52
+ kind = d.get("kind", "") or "(unset)"
53
+ purpose = d.get("purpose", "") or "(unset)"
54
+ priority = d.get("priority", "") or "(unset)"
55
+ persona = d.get("persona", "") or "(unset)"
56
+ print(f"session: {sid}")
57
+ print(f" name: {name}")
58
+ print(f" kind: {kind}")
59
+ print(f" purpose: {purpose}")
60
+ print(f" priority: {priority}")
61
+ print(f" persona: {persona}")
62
+ PY
63
+ }
64
+
65
+ set_field() {
66
+ FIELD="$1" VAL="$2" META_PATH="$META" python3 - <<'PY'
67
+ import json, os
68
+ p = os.environ["META_PATH"]
69
+ d = json.load(open(p))
70
+ field = os.environ["FIELD"]
71
+ d[field] = os.environ["VAL"]
72
+ json.dump(d, open(p, "w"), indent=2)
73
+ print(f"{field}: {d[field] or '(unset)'}")
74
+ PY
75
+ }
76
+
77
+ case "$ACTION" in
78
+ ""|show)
79
+ print_meta
80
+ ;;
81
+ clear)
82
+ META_PATH="$META" python3 - <<'PY'
83
+ import json, os
84
+ p = os.environ["META_PATH"]
85
+ d = json.load(open(p))
86
+ for k in ("name", "kind", "purpose", "priority"):
87
+ d[k] = ""
88
+ json.dump(d, open(p, "w"), indent=2)
89
+ print("Cleared name, kind, purpose, priority.")
90
+ PY
91
+ ;;
92
+ set-name)
93
+ set_field "name" "$*"
94
+ ;;
95
+ set-kind)
96
+ case "$1" in
97
+ l|long|long-running) VAL="long-running" ;;
98
+ o|outcome) VAL="outcome" ;;
99
+ "") VAL="" ;;
100
+ *) echo "kind '$1' not recognized — use long or outcome"; exit 1 ;;
101
+ esac
102
+ set_field "kind" "$VAL"
103
+ ;;
104
+ set-purpose)
105
+ set_field "purpose" "$*"
106
+ ;;
107
+ set-priority)
108
+ case "$1" in
109
+ l|low) VAL="low" ;;
110
+ m|med|medium) VAL="med" ;;
111
+ h|high) VAL="high" ;;
112
+ "") VAL="" ;;
113
+ *) echo "priority '$1' not recognized — use low, med, or high"; exit 1 ;;
114
+ esac
115
+ set_field "priority" "$VAL"
116
+ ;;
117
+ *)
118
+ echo "Usage: session-meta.sh [show|clear|set-name|set-kind|set-purpose|set-priority] [args]"
119
+ exit 1
120
+ ;;
121
+ esac
@@ -1,22 +1,50 @@
1
1
  #!/bin/bash
2
- # UV Suite Hook: Session start — record start time
2
+ # UV Suite Hook: Session start — record start time, fire bootstrap event.
3
3
  # Event: SessionStart
4
4
 
5
5
  STATE_DIR="${CLAUDE_PROJECT_DIR:-.}/.uv-suite-state"
6
- mkdir -p "$STATE_DIR"
6
+ SESSIONS_DIR="$STATE_DIR/sessions"
7
+ mkdir -p "$SESSIONS_DIR"
7
8
 
8
- # Record session start time
9
+ # Per-cwd start timestamp (legacy — used by status-line for "today" totals)
9
10
  date +%s > "$STATE_DIR/session-start.txt"
10
11
 
11
- # Track today's total active time
12
12
  TODAY=$(date +%Y-%m-%d)
13
13
  TODAY_FILE="$STATE_DIR/active-$TODAY.txt"
14
- if [ ! -f "$TODAY_FILE" ]; then
15
- echo "0" > "$TODAY_FILE"
14
+ [ ! -f "$TODAY_FILE" ] && echo "0" > "$TODAY_FILE"
15
+
16
+ # Resolve UVS session id (set by uv.sh; fall back to current-session pointer)
17
+ SID="${UVS_SESSION_ID:-}"
18
+ if [ -z "$SID" ] && [ -f "$STATE_DIR/current-session.txt" ]; then
19
+ SID=$(cat "$STATE_DIR/current-session.txt" 2>/dev/null)
20
+ fi
21
+
22
+ # Build a bootstrap payload that includes session metadata so the dashboard
23
+ # learns about the session before any tool calls happen.
24
+ INPUT=$(cat 2>/dev/null)
25
+ [ -z "$INPUT" ] && INPUT='{}'
26
+
27
+ PAYLOAD=""
28
+ META_FILE=""
29
+ [ -n "$SID" ] && META_FILE="$SESSIONS_DIR/$SID.json"
30
+
31
+ if [ -n "$META_FILE" ] && [ -f "$META_FILE" ] && command -v jq >/dev/null 2>&1; then
32
+ PAYLOAD=$(echo "$INPUT" | jq -c --slurpfile m "$META_FILE" '
33
+ . + {
34
+ uvs_session_id: ($m[0].uvs_session_id // ""),
35
+ session_name: ($m[0].name // ""),
36
+ session_kind: ($m[0].kind // ""),
37
+ session_purpose: ($m[0].purpose // ""),
38
+ session_priority: ($m[0].priority // ""),
39
+ persona: ($m[0].persona // ""),
40
+ cwd: (.cwd // $m[0].cwd // "")
41
+ }' 2>/dev/null)
42
+ fi
43
+
44
+ if [ -z "$PAYLOAD" ]; then
45
+ PAYLOAD=$(printf '{"uvs_session_id":"%s","cwd":"%s"}' "$SID" "${CLAUDE_PROJECT_DIR:-.}")
16
46
  fi
17
47
 
18
- # Send to watchtower
19
- echo '{"session_id":"'$(cat "$STATE_DIR/session-start.txt")'","cwd":"'${CLAUDE_PROJECT_DIR:-.}'"}' | \
20
- "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/watchtower-send.sh" "SessionStart" 2>/dev/null
48
+ echo "$PAYLOAD" | "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/watchtower-send.sh" "SessionStart" 2>/dev/null
21
49
 
22
50
  exit 0
@@ -1,46 +1,71 @@
1
1
  #!/bin/bash
2
- # UV Suite Hook: Status line — show session duration and persona
2
+ # UV Suite Hook: Status line — show session label, persona, duration.
3
3
  # Event: statusLine (rendered continuously)
4
4
 
5
5
  STATE_DIR="${CLAUDE_PROJECT_DIR:-.}/.uv-suite-state"
6
+ SESSIONS_DIR="$STATE_DIR/sessions"
6
7
  START_FILE="$STATE_DIR/session-start.txt"
7
8
 
8
- # Session duration
9
- if [ -f "$START_FILE" ]; then
9
+ # Resolve UVS session id
10
+ SID="${UVS_SESSION_ID:-}"
11
+ if [ -z "$SID" ] && [ -f "$STATE_DIR/current-session.txt" ]; then
12
+ SID=$(cat "$STATE_DIR/current-session.txt" 2>/dev/null)
13
+ fi
14
+ META_FILE=""
15
+ [ -n "$SID" ] && META_FILE="$SESSIONS_DIR/$SID.json"
16
+
17
+ # Pull metadata fields if available
18
+ NAME=""; PERSONA=""; PRIORITY=""; STARTED_AT=""
19
+ if [ -n "$META_FILE" ] && [ -f "$META_FILE" ]; then
20
+ if command -v jq >/dev/null 2>&1; then
21
+ NAME=$(jq -r '.name // ""' "$META_FILE" 2>/dev/null)
22
+ PERSONA=$(jq -r '.persona // ""' "$META_FILE" 2>/dev/null)
23
+ PRIORITY=$(jq -r '.priority // ""' "$META_FILE" 2>/dev/null)
24
+ STARTED_AT=$(jq -r '.started_at // ""' "$META_FILE" 2>/dev/null)
25
+ fi
26
+ fi
27
+
28
+ # Session duration: prefer per-session started_at, fall back to per-cwd legacy file
29
+ NOW=$(date +%s)
30
+ START=""
31
+ if [ -n "$STARTED_AT" ] && [ "$STARTED_AT" != "null" ]; then
32
+ START="$STARTED_AT"
33
+ elif [ -f "$START_FILE" ]; then
10
34
  START=$(cat "$START_FILE")
11
- NOW=$(date +%s)
35
+ fi
36
+ if [ -n "$START" ]; then
12
37
  ELAPSED=$((NOW - START))
13
38
  MINS=$((ELAPSED / 60))
14
-
15
- if [ "$MINS" -ge 180 ]; then
16
- SESSION="session ${MINS}m (!! take a break)"
17
- elif [ "$MINS" -ge 90 ]; then
18
- SESSION="session ${MINS}m (break soon)"
19
- elif [ "$MINS" -ge 45 ]; then
20
- SESSION="session ${MINS}m"
21
- else
22
- SESSION="session ${MINS}m"
23
- fi
24
39
  else
25
- SESSION="session 0m"
40
+ MINS=0
41
+ fi
42
+
43
+ if [ "$MINS" -ge 180 ]; then SESSION="${MINS}m (!! take a break)"
44
+ elif [ "$MINS" -ge 90 ]; then SESSION="${MINS}m (break soon)"
45
+ else SESSION="${MINS}m"
26
46
  fi
27
47
 
28
- # Today's total active time
48
+ # Today's total active time (per cwd)
29
49
  TODAY=$(date +%Y-%m-%d)
30
50
  TODAY_FILE="$STATE_DIR/active-$TODAY.txt"
51
+ TODAY_STR=""
31
52
  if [ -f "$TODAY_FILE" ]; then
32
53
  TODAY_TOTAL=$(cat "$TODAY_FILE")
33
- CURRENT_MINS=${MINS:-0}
34
- TODAY_NOW=$((TODAY_TOTAL + CURRENT_MINS))
54
+ TODAY_NOW=$((TODAY_TOTAL + MINS))
35
55
  TODAY_STR=" · today ${TODAY_NOW}m"
36
- else
37
- TODAY_STR=""
38
56
  fi
39
57
 
40
- # Persona from settings path (best-effort detection)
41
- PERSONA=""
42
- if [ -f "$CLAUDE_PROJECT_DIR/.claude/settings.local.json" ]; then
43
- PERSONA=$(grep -o '"effort"[^,]*' "$CLAUDE_PROJECT_DIR/.claude/settings.local.json" 2>/dev/null | head -1)
58
+ # Build label
59
+ LABEL_PARTS=()
60
+ if [ -n "$NAME" ] && [ "$NAME" != "null" ]; then
61
+ LABEL_PARTS+=("$NAME")
62
+ else
63
+ LABEL_PARTS+=("(unlabeled)")
44
64
  fi
65
+ [ -n "$PERSONA" ] && [ "$PERSONA" != "null" ] && LABEL_PARTS+=("[$PERSONA]")
66
+ [ -n "$PRIORITY" ] && [ "$PRIORITY" != "null" ] && LABEL_PARTS+=("p:$PRIORITY")
67
+
68
+ # Join with spaces
69
+ LABEL_STR="${LABEL_PARTS[*]}"
45
70
 
46
- echo "UV Suite · $SESSION$TODAY_STR"
71
+ echo "UV Suite · $LABEL_STR · ${SESSION}${TODAY_STR}"
@@ -1,34 +1,66 @@
1
1
  #!/bin/bash
2
- # UV Suite Hook Helper: Send event to Watchtower server
2
+ # UV Suite Hook Helper: Send event to Watchtower server.
3
3
  # Called by other hooks or directly from persona hook config.
4
4
  # Non-blocking. Fails silently if server not running.
5
5
  #
6
6
  # Usage from hook config:
7
7
  # "command": ".claude/hooks/watchtower-send.sh PostToolUse"
8
- # The hook input JSON comes via stdin from Claude Code.
8
+ # Hook input JSON arrives via stdin from Claude Code.
9
+ #
10
+ # Merges UV Suite session metadata (name, kind, purpose, priority, persona)
11
+ # from .uv-suite-state/sessions/$UVS_SESSION_ID.json into every payload so
12
+ # the dashboard can group + label sessions.
9
13
 
10
14
  EVENT_TYPE="${1:-Unknown}"
11
15
  INPUT=$(cat)
12
16
  WATCHTOWER_URL="${UVS_WATCHTOWER_URL:-http://localhost:4200}"
13
17
 
14
- # Forward the full hook input to the watchtower, adding event_type and source_app
15
- # jq merges the original JSON with our extra fields
16
- PAYLOAD=$(echo "$INPUT" | jq -c ". + {
17
- event_type: \"$EVENT_TYPE\",
18
- source_app: (.cwd // \"\" | split(\"/\") | last),
19
- _hook_ts: now
20
- }" 2>/dev/null)
18
+ STATE_DIR="${CLAUDE_PROJECT_DIR:-.}/.uv-suite-state"
19
+ SESSIONS_DIR="$STATE_DIR/sessions"
20
+
21
+ # Resolve UVS session id: env first, then current-session pointer
22
+ SID="${UVS_SESSION_ID:-}"
23
+ if [ -z "$SID" ] && [ -f "$STATE_DIR/current-session.txt" ]; then
24
+ SID=$(cat "$STATE_DIR/current-session.txt" 2>/dev/null)
25
+ fi
26
+ META_FILE=""
27
+ [ -n "$SID" ] && META_FILE="$SESSIONS_DIR/$SID.json"
28
+
29
+ PAYLOAD=""
30
+ if command -v jq >/dev/null 2>&1; then
31
+ if [ -n "$META_FILE" ] && [ -f "$META_FILE" ]; then
32
+ PAYLOAD=$(echo "$INPUT" | jq -c --arg etype "$EVENT_TYPE" --slurpfile m "$META_FILE" '
33
+ . + {
34
+ event_type: $etype,
35
+ source_app: (.cwd // "" | split("/") | last),
36
+ uvs_session_id: ($m[0].uvs_session_id // ""),
37
+ session_name: ($m[0].name // ""),
38
+ session_kind: ($m[0].kind // ""),
39
+ session_purpose: ($m[0].purpose // ""),
40
+ session_priority: ($m[0].priority // ""),
41
+ persona: ($m[0].persona // ""),
42
+ _hook_ts: now
43
+ }' 2>/dev/null)
44
+ else
45
+ PAYLOAD=$(echo "$INPUT" | jq -c --arg etype "$EVENT_TYPE" '
46
+ . + {
47
+ event_type: $etype,
48
+ source_app: (.cwd // "" | split("/") | last),
49
+ _hook_ts: now
50
+ }' 2>/dev/null)
51
+ fi
52
+ fi
21
53
 
22
- # Fallback if jq isn't available or fails
54
+ # Fallback when jq is missing or produced nothing usable
23
55
  if [ -z "$PAYLOAD" ] || [ "$PAYLOAD" = "null" ]; then
24
56
  SESSION_ID=$(echo "$INPUT" | grep -o '"session_id":"[^"]*"' | head -1 | cut -d'"' -f4)
25
57
  TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"' -f4)
26
58
  CWD=$(echo "$INPUT" | grep -o '"cwd":"[^"]*"' | head -1 | cut -d'"' -f4)
27
59
  SOURCE_APP=$(basename "$CWD" 2>/dev/null)
28
- PAYLOAD="{\"event_type\":\"$EVENT_TYPE\",\"session_id\":\"$SESSION_ID\",\"source_app\":\"$SOURCE_APP\",\"tool_name\":\"$TOOL_NAME\",\"cwd\":\"$CWD\"}"
60
+ PAYLOAD=$(printf '{"event_type":"%s","session_id":"%s","uvs_session_id":"%s","source_app":"%s","tool_name":"%s","cwd":"%s"}' \
61
+ "$EVENT_TYPE" "$SESSION_ID" "$SID" "$SOURCE_APP" "$TOOL_NAME" "$CWD")
29
62
  fi
30
63
 
31
- # Send to watchtower (non-blocking, fire-and-forget)
32
64
  curl -s -X POST "$WATCHTOWER_URL/events" \
33
65
  -H "Content-Type: application/json" \
34
66
  -d "$PAYLOAD" \
package/install.sh CHANGED
@@ -102,7 +102,7 @@ cp "$UV_SUITE_DIR/agents/cursor/"*.mdc "$PROJECT_ROOT/.cursor/rules/"
102
102
  echo " ✓ .cursor/rules/*.mdc installed"
103
103
 
104
104
  # --- Install skills (slash commands) ---
105
- echo "Installing 10 skills..."
105
+ echo "Installing skills..."
106
106
  for skill_dir in "$UV_SUITE_DIR/skills/"*/; do
107
107
  skill_name=$(basename "$skill_dir")
108
108
  mkdir -p "$TARGET_DIR/skills/$skill_name"
@@ -110,7 +110,7 @@ for skill_dir in "$UV_SUITE_DIR/skills/"*/; do
110
110
  done
111
111
  echo " ✓ /map-codebase, /spec, /architect, /review, /write-tests"
112
112
  echo " ✓ /write-evals, /slop-check, /prototype, /security-review"
113
- echo " ✓ /map-stack"
113
+ echo " ✓ /map-stack, /session-init"
114
114
 
115
115
  # --- Install hooks ---
116
116
  echo "Installing hook scripts..."
@@ -122,7 +122,9 @@ echo " ✓ block-destructive.sh (PreToolUse: block rm -rf, force push, etc.)"
122
122
  echo " ✓ session-start.sh (SessionStart: track session start time)"
123
123
  echo " ✓ session-timer.sh (PostToolUse: warn at 45/90/180 min)"
124
124
  echo " ✓ session-end.sh (Stop: reflection + duration + review reminder)"
125
- echo " ✓ status-line.sh (statusLine: show session time continuously)"
125
+ echo " ✓ session-label-nag.sh (UserPromptSubmit: nudge to /session-init)"
126
+ echo " ✓ session-meta.sh (helper used by /session-init)"
127
+ echo " ✓ status-line.sh (statusLine: show session label + time)"
126
128
  echo " + Real-time slop check (Haiku prompt hook, wired in settings.json)"
127
129
 
128
130
  # --- Install guardrail rules (Sport only) ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uv-suite",
3
- "version": "0.26.1",
3
+ "version": "0.26.3",
4
4
  "description": "Portable framework for AI-assisted software development. 10 agents, 9 skills, 5 hooks, 4 personas. Works with Claude Code, Cursor, and Codex.",
5
5
  "author": "Utsav Anand",
6
6
  "license": "MIT",