sync: auto-sync from GURU-KALI at 2026-05-27 20:20:56
Author: Mike Swanson Machine: GURU-KALI Timestamp: 2026-05-27 20:20:56
This commit is contained in:
@@ -22,52 +22,118 @@ if [ -f "$IDENTITY_FILE" ]; then
|
||||
USER_ALIAS=$(jq -r '.user // empty' "$IDENTITY_FILE" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Sanitize JSON: the coord API sometimes returns message bodies with unescaped
|
||||
# control chars (U+0000-U+001F) — invalid JSON per RFC 8259, which makes jq
|
||||
# abort with "Invalid string: control characters ... must be escaped". Round-
|
||||
# trip through python's json.loads(strict=False) (which accepts them) and
|
||||
# re-emit properly escaped JSON so jq downstream works on every payload.
|
||||
sanitize_json() {
|
||||
python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
print(json.dumps(json.loads(sys.stdin.read(), strict=False)))
|
||||
except Exception:
|
||||
pass
|
||||
' 2>/dev/null
|
||||
}
|
||||
|
||||
# --- Unread messages ---------------------------------------------------------
|
||||
|
||||
# Query for messages addressed to full session ID
|
||||
result=$(curl -s --connect-timeout 3 "${API}/api/coord/messages?to_session=${SESSION}&unread_only=true" 2>/dev/null)
|
||||
result=$(curl -s --connect-timeout 3 "${API}/api/coord/messages?to_session=${SESSION}&unread_only=true" 2>/dev/null | sanitize_json)
|
||||
|
||||
# Also query for messages addressed to the short username alias (e.g. "howard")
|
||||
result_alias=""
|
||||
if [ -n "$USER_ALIAS" ] && [ "$USER_ALIAS" != "$SESSION" ]; then
|
||||
result_alias=$(curl -s --connect-timeout 3 "${API}/api/coord/messages?to_session=${USER_ALIAS}&unread_only=true" 2>/dev/null)
|
||||
result_alias=$(curl -s --connect-timeout 3 "${API}/api/coord/messages?to_session=${USER_ALIAS}&unread_only=true" 2>/dev/null | sanitize_json)
|
||||
fi
|
||||
|
||||
# Merge both result sets (combine .messages arrays, recompute total)
|
||||
# Merge personal + alias result sets (combine .messages arrays, recompute total).
|
||||
# IMPORTANT: use `printf '%s'`, NOT `echo`, to pipe JSON to jq — bash `echo`
|
||||
# interprets backslash escapes (e.g. \n inside string bodies) and corrupts JSON.
|
||||
if [ -n "$result_alias" ]; then
|
||||
alias_msgs=$(echo "$result_alias" | jq '.messages // []' 2>/dev/null)
|
||||
alias_msgs=$(printf '%s' "$result_alias" | jq '.messages // []' 2>/dev/null)
|
||||
if [ -n "$alias_msgs" ] && [ "$alias_msgs" != "[]" ] && [ "$alias_msgs" != "null" ]; then
|
||||
if [ -n "$result" ]; then
|
||||
result=$(echo "$result" "$result_alias" | jq -s '{total: (.[0].total + .[1].total), messages: (.[0].messages + .[1].messages)}' 2>/dev/null)
|
||||
result=$(printf '%s\n%s\n' "$result" "$result_alias" | jq -s '{total: (.[0].total + .[1].total), messages: (.[0].messages + .[1].messages)}' 2>/dev/null)
|
||||
else
|
||||
result="$result_alias"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$result" ]; then
|
||||
count=$(echo "$result" | jq '.total' 2>/dev/null)
|
||||
if [ -n "$count" ] && [ "$count" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "UNREAD COORD MESSAGES ($count)"
|
||||
echo "============================================================"
|
||||
echo "$result" | jq -r '.messages[] | "FROM: \(.from_session)\nDATE: \(.created_at)\nSUBJECT: \(.subject)\n\nMESSAGE:\n\(.body)\n---"'
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
# --- Broadcasts (to_session=ALL_SESSIONS) -----------------------------------
|
||||
# Broadcasts share a single server-side read_at field, so PUT /read on a
|
||||
# broadcast would clobber it for every other machine that hasn't seen it yet.
|
||||
# Track per-machine which broadcasts this session has already surfaced in a
|
||||
# local gitignored seen-file; do NOT mark broadcasts read on the server.
|
||||
SEEN_FILE="${SCRIPT_DIR}/coord-broadcasts-seen"
|
||||
[ -f "$SEEN_FILE" ] || : > "$SEEN_FILE"
|
||||
|
||||
# Fire a Windows toast so the user sees it even if not watching the terminal
|
||||
toast_body=$(echo "$result" | jq -r '[.messages[] | .from_session + ": " + .subject] | join(", ")' | tr -d '\r')
|
||||
notify_ps1=$(cygpath -w "${SCRIPT_DIR}/scripts/notify.ps1" 2>/dev/null || echo "${SCRIPT_DIR}/scripts/notify.ps1" | sed 's|^/\([a-zA-Z]\)/|\1:/|')
|
||||
powershell.exe -NonInteractive -NoProfile -Command \
|
||||
"& '$notify_ps1' -Title 'ClaudeTools: $count new message(s)' -Message '$toast_body'" \
|
||||
>/dev/null 2>&1 &
|
||||
bcast_raw=$(curl -s --connect-timeout 3 "${API}/api/coord/messages?to_session=ALL_SESSIONS&unread_only=true&limit=100" 2>/dev/null | sanitize_json)
|
||||
bcast_msgs="[]"
|
||||
if [ -n "$bcast_raw" ]; then
|
||||
# JSON array of already-seen broadcast IDs (lines from seen-file; empty -> [])
|
||||
seen_json=$(jq -R . "$SEEN_FILE" | jq -s 'map(select(length>0))' 2>/dev/null)
|
||||
[ -z "$seen_json" ] && seen_json='[]'
|
||||
# Normalize this session for self-broadcast filter (drop .local for compare)
|
||||
session_norm=$(printf '%s' "$SESSION" | tr 'A-Z' 'a-z')
|
||||
# Bind each message to $m so .id / .from_session inside index() and sub()
|
||||
# resolve against the message, not against $seen (jq scoping gotcha).
|
||||
bcast_msgs=$(printf '%s' "$bcast_raw" | jq --argjson seen "$seen_json" --arg self "$session_norm" '
|
||||
(.messages // [])
|
||||
| map(. as $m | select(
|
||||
(($seen | index($m.id)) == null)
|
||||
and (($m.from_session | ascii_downcase | sub("\\.local/"; "/")) != $self)
|
||||
))
|
||||
' 2>/dev/null)
|
||||
[ -z "$bcast_msgs" ] && bcast_msgs="[]"
|
||||
fi
|
||||
|
||||
# Mark all fetched messages as read immediately
|
||||
echo "$result" | jq -r '.messages[].id' | tr -d '\r' | while read -r id; do
|
||||
curl -s -X PUT "${API}/api/coord/messages/${id}/read" >/dev/null 2>&1
|
||||
done
|
||||
# --- Combined display (personal + alias + filtered broadcasts) --------------
|
||||
# NB: do NOT use ${result:-{}} as a default — bash parameter expansion treats
|
||||
# the first '}' as the close, producing '{}}' and breaking jq downstream.
|
||||
result_safe="$result"
|
||||
[ -z "$result_safe" ] && result_safe='{"messages":[]}'
|
||||
personal_msgs=$(printf '%s' "$result_safe" | jq '.messages // []' 2>/dev/null)
|
||||
[ -z "$personal_msgs" ] && personal_msgs="[]"
|
||||
total_count=$(jq -n --argjson p "$personal_msgs" --argjson b "$bcast_msgs" '($p|length) + ($b|length)' 2>/dev/null)
|
||||
[ -z "$total_count" ] && total_count=0
|
||||
|
||||
if [ "$total_count" -gt 0 ] 2>/dev/null; then
|
||||
combined=$(jq -n --argjson p "$personal_msgs" --argjson b "$bcast_msgs" '$p + $b')
|
||||
bcast_count=$(printf '%s' "$bcast_msgs" | jq 'length' 2>/dev/null)
|
||||
[ -z "$bcast_count" ] && bcast_count=0
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
if [ "$bcast_count" -gt 0 ] 2>/dev/null; then
|
||||
echo "UNREAD COORD MESSAGES (${total_count}, including ${bcast_count} broadcast)"
|
||||
else
|
||||
echo "UNREAD COORD MESSAGES (${total_count})"
|
||||
fi
|
||||
echo "============================================================"
|
||||
printf '%s' "$combined" | jq -r '.[] | "FROM: \(.from_session)\nDATE: \(.created_at)\nSUBJECT: \(.subject)\n\nMESSAGE:\n\(.body)\n---"'
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
|
||||
# Fire a Windows toast so the user sees it even if not watching the terminal
|
||||
toast_body=$(printf '%s' "$combined" | jq -r '[.[] | .from_session + ": " + .subject] | join(", ")' | tr -d '\r')
|
||||
notify_ps1=$(cygpath -w "${SCRIPT_DIR}/scripts/notify.ps1" 2>/dev/null || echo "${SCRIPT_DIR}/scripts/notify.ps1" | sed 's|^/\([a-zA-Z]\)/|\1:/|')
|
||||
powershell.exe -NonInteractive -NoProfile -Command \
|
||||
"& '$notify_ps1' -Title 'ClaudeTools: ${total_count} new message(s)' -Message '$toast_body'" \
|
||||
>/dev/null 2>&1 &
|
||||
|
||||
# Mark personal + alias messages as read on the server (NOT broadcasts).
|
||||
printf '%s' "$personal_msgs" | jq -r '.[].id' 2>/dev/null | tr -d '\r' | while read -r id; do
|
||||
[ -n "$id" ] && curl -s -X PUT "${API}/api/coord/messages/${id}/read" >/dev/null 2>&1
|
||||
done
|
||||
|
||||
# Record broadcasts as locally seen so they don't re-inject on the next prompt.
|
||||
# Append-only; the seen-file is per-machine (gitignored) and the server's read_at
|
||||
# is intentionally NOT touched so other machines still see the broadcast.
|
||||
printf '%s' "$bcast_msgs" | jq -r '.[].id' 2>/dev/null | tr -d '\r' | while read -r id; do
|
||||
[ -n "$id" ] && printf '%s\n' "$id" >> "$SEEN_FILE"
|
||||
done
|
||||
fi
|
||||
|
||||
# --- Active locks (dev mode only) -------------------------------------------
|
||||
@@ -78,13 +144,13 @@ current_mode=""
|
||||
if [ "$current_mode" = "dev" ]; then
|
||||
locks=$(curl -s --connect-timeout 3 "${API}/api/coord/locks" 2>/dev/null)
|
||||
if [ -n "$locks" ]; then
|
||||
lock_count=$(echo "$locks" | jq '.total' 2>/dev/null)
|
||||
lock_count=$(printf '%s' "$locks" | jq '.total' 2>/dev/null)
|
||||
if [ -n "$lock_count" ] && [ "$lock_count" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "[WARNING] ACTIVE LOCKS ($lock_count) — check before editing"
|
||||
echo "============================================================"
|
||||
echo "$locks" | jq -r '.locks[] | "[DEV MODE] LOCK: \(.project_key) / \(.resource)\n Held by: \(.session_id)\n Reason: \(.description // "none")\n Expires: \(.expires_at // "unknown")\n---"'
|
||||
printf '%s' "$locks" | jq -r '.locks[] | "[DEV MODE] LOCK: \(.project_key) / \(.resource)\n Held by: \(.session_id)\n Reason: \(.description // "none")\n Expires: \(.expires_at // "unknown")\n---"'
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@ tmp-remediation/
|
||||
.claude/settings.local.json
|
||||
.claude/identity.json
|
||||
.claude/current-mode
|
||||
.claude/coord-broadcasts-seen
|
||||
|
||||
# /autotask command — kept local/undistributed (Syncro is the default PSA; Autotask is opt-in).
|
||||
# Remove this line to distribute /autotask to the fleet. See .claude/memory/feedback_psa_default_syncro.md
|
||||
|
||||
Reference in New Issue
Block a user