uv-suite 0.26.1 → 0.26.2
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.
- package/hooks/confirm-prompt.sh +51 -0
- package/hooks/session-label-nag.sh +63 -0
- package/hooks/session-meta.sh +121 -0
- package/hooks/session-start.sh +37 -9
- package/hooks/status-line.sh +50 -25
- package/hooks/watchtower-send.sh +44 -12
- package/install.sh +5 -3
- package/package.json +1 -1
- package/personas/auto.json +77 -19
- package/personas/professional.json +29 -7
- package/personas/spike.json +72 -18
- package/personas/sport.json +72 -18
- package/skills/checkpoint/SKILL.md +17 -5
- package/skills/confirm/SKILL.md +35 -0
- package/skills/restore/SKILL.md +5 -2
- package/skills/session-init/SKILL.md +45 -0
- package/uv.sh +84 -14
- package/watchtower/dashboard.html +125 -20
- package/watchtower/events.json +1 -22
|
@@ -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
|
package/hooks/session-start.sh
CHANGED
|
@@ -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
|
-
|
|
6
|
+
SESSIONS_DIR="$STATE_DIR/sessions"
|
|
7
|
+
mkdir -p "$SESSIONS_DIR"
|
|
7
8
|
|
|
8
|
-
#
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
package/hooks/status-line.sh
CHANGED
|
@@ -1,46 +1,71 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# UV Suite Hook: Status line — show session
|
|
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
|
-
#
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
41
|
-
|
|
42
|
-
if [ -
|
|
43
|
-
|
|
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}"
|
package/hooks/watchtower-send.sh
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
|
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
|
|
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 " ✓
|
|
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.
|
|
3
|
+
"version": "0.26.2",
|
|
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",
|
package/personas/auto.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
|
3
3
|
"effort": "max",
|
|
4
|
-
|
|
5
4
|
"permissions": {
|
|
6
5
|
"allow": [
|
|
7
6
|
"Read(*)",
|
|
@@ -48,14 +47,22 @@
|
|
|
48
47
|
"Bash(git push --force * master)"
|
|
49
48
|
]
|
|
50
49
|
},
|
|
51
|
-
|
|
52
50
|
"hooks": {
|
|
53
51
|
"SessionStart": [
|
|
54
52
|
{
|
|
55
53
|
"matcher": "*",
|
|
56
54
|
"hooks": [
|
|
57
|
-
{
|
|
58
|
-
|
|
55
|
+
{
|
|
56
|
+
"type": "command",
|
|
57
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh",
|
|
58
|
+
"timeout": 5
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"type": "command",
|
|
62
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh SessionStart",
|
|
63
|
+
"timeout": 2,
|
|
64
|
+
"async": true
|
|
65
|
+
}
|
|
59
66
|
]
|
|
60
67
|
}
|
|
61
68
|
],
|
|
@@ -63,24 +70,63 @@
|
|
|
63
70
|
{
|
|
64
71
|
"matcher": "*",
|
|
65
72
|
"hooks": [
|
|
66
|
-
{
|
|
73
|
+
{
|
|
74
|
+
"type": "command",
|
|
75
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/confirm-prompt.sh",
|
|
76
|
+
"timeout": 3
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"type": "command",
|
|
80
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-label-nag.sh",
|
|
81
|
+
"timeout": 3
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"type": "command",
|
|
85
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh UserPromptSubmit",
|
|
86
|
+
"timeout": 2,
|
|
87
|
+
"async": true
|
|
88
|
+
}
|
|
67
89
|
]
|
|
68
90
|
}
|
|
69
91
|
],
|
|
70
92
|
"PermissionRequest": [
|
|
71
|
-
{
|
|
72
|
-
|
|
73
|
-
|
|
93
|
+
{
|
|
94
|
+
"matcher": "*",
|
|
95
|
+
"hooks": [
|
|
96
|
+
{
|
|
97
|
+
"type": "command",
|
|
98
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PermissionRequest",
|
|
99
|
+
"timeout": 2,
|
|
100
|
+
"async": true
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
74
104
|
],
|
|
75
105
|
"Notification": [
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
106
|
+
{
|
|
107
|
+
"matcher": "*",
|
|
108
|
+
"hooks": [
|
|
109
|
+
{
|
|
110
|
+
"type": "command",
|
|
111
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh Notification",
|
|
112
|
+
"timeout": 2,
|
|
113
|
+
"async": true
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
}
|
|
79
117
|
],
|
|
80
118
|
"PostToolUseFailure": [
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
119
|
+
{
|
|
120
|
+
"matcher": "*",
|
|
121
|
+
"hooks": [
|
|
122
|
+
{
|
|
123
|
+
"type": "command",
|
|
124
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PostToolUseFailure",
|
|
125
|
+
"timeout": 2,
|
|
126
|
+
"async": true
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
84
130
|
],
|
|
85
131
|
"PreToolUse": [
|
|
86
132
|
{
|
|
@@ -99,8 +145,17 @@
|
|
|
99
145
|
{
|
|
100
146
|
"matcher": "*",
|
|
101
147
|
"hooks": [
|
|
102
|
-
{
|
|
103
|
-
|
|
148
|
+
{
|
|
149
|
+
"type": "command",
|
|
150
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-timer.sh",
|
|
151
|
+
"timeout": 5
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"type": "command",
|
|
155
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PostToolUse",
|
|
156
|
+
"timeout": 2,
|
|
157
|
+
"async": true
|
|
158
|
+
}
|
|
104
159
|
]
|
|
105
160
|
},
|
|
106
161
|
{
|
|
@@ -119,14 +174,17 @@
|
|
|
119
174
|
{
|
|
120
175
|
"matcher": "*",
|
|
121
176
|
"hooks": [
|
|
122
|
-
{
|
|
177
|
+
{
|
|
178
|
+
"type": "command",
|
|
179
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-end.sh",
|
|
180
|
+
"timeout": 10
|
|
181
|
+
}
|
|
123
182
|
]
|
|
124
183
|
}
|
|
125
184
|
]
|
|
126
185
|
},
|
|
127
|
-
|
|
128
186
|
"statusLine": {
|
|
129
187
|
"type": "command",
|
|
130
188
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/status-line.sh"
|
|
131
189
|
}
|
|
132
|
-
}
|
|
190
|
+
}
|