sync: auto-sync from GURU-5070 at 2026-07-01 15:49:56
Author: Mike Swanson Machine: GURU-5070 Timestamp: 2026-07-01 15:49:56
This commit is contained in:
@@ -99,17 +99,45 @@ if [ "$MODE" = "dm" ]; then
|
||||
TARGET="$CHID"
|
||||
fi
|
||||
|
||||
# --- post the message (printf | --data-binary @- — direct -d mangles multiline JSON) ---
|
||||
RESP="$(printf '%s' "$(jq -nc --arg c "$MSG" '{content:$c}')" | \
|
||||
curl -s -m 15 -w $'\n%{http_code}' "${auth[@]}" \
|
||||
-X POST "$API/channels/${TARGET}/messages" --data-binary @-)"
|
||||
HTTP="$(printf '%s' "$RESP" | tail -n1)"
|
||||
BODY="$(printf '%s' "$RESP" | sed '$d')"
|
||||
# --- chunk: Discord caps content at 2000 chars (code 50035 on overflow).
|
||||
# This tool's job is delivering LONG content intact, so split into <=1900-char
|
||||
# pieces (preferring newline boundaries) and send them in order — no markers
|
||||
# added, so the receiver can copy-paste the sequence back together verbatim.
|
||||
LIMIT=1900
|
||||
CHUNKS=()
|
||||
rest="$MSG"
|
||||
while [ "${#rest}" -gt "$LIMIT" ]; do
|
||||
piece="${rest:0:$LIMIT}"
|
||||
at_nl="${piece%$'\n'*}" # cut at the last newline in the window
|
||||
if [ "${#at_nl}" -lt "${#piece}" ] && [ "${#at_nl}" -gt 0 ]; then
|
||||
piece="$at_nl"
|
||||
fi
|
||||
CHUNKS+=("$piece")
|
||||
rest="${rest:${#piece}}"
|
||||
rest="${rest#$'\n'}"
|
||||
done
|
||||
CHUNKS+=("$rest")
|
||||
|
||||
if [ "$HTTP" = "200" ]; then
|
||||
# --- post the message (jq | --data-binary @- — argv/-d mangles multiline + non-ASCII JSON) ---
|
||||
N=${#CHUNKS[@]}
|
||||
i=0
|
||||
for piece in "${CHUNKS[@]}"; do
|
||||
i=$((i+1))
|
||||
RESP="$(printf '%s' "$(jq -nc --arg c "$piece" '{content:$c}')" | \
|
||||
curl -s -m 15 -w $'\n%{http_code}' "${auth[@]}" \
|
||||
-X POST "$API/channels/${TARGET}/messages" --data-binary @-)"
|
||||
HTTP="$(printf '%s' "$RESP" | tail -n1)"
|
||||
BODY="$(printf '%s' "$RESP" | sed '$d')"
|
||||
if [ "$HTTP" != "200" ]; then
|
||||
echo "[ERROR] discord-dm: Discord returned ${HTTP:-no-response} on chunk ${i}/${N} — ${BODY}" >&2
|
||||
bash "$ROOT/.claude/scripts/log-skill-error.sh" "discord-dm" "Discord send to $LABEL failed (chunk ${i}/${N})" --context "http=${HTTP:-none} resp=${BODY:0:80}" >/dev/null 2>&1
|
||||
exit 3
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$N" -gt 1 ]; then
|
||||
echo "[OK] discord-dm: sent to ${LABEL} in ${N} chunks (message_id=$(printf '%s' "$BODY" | jq -r '.id // empty'))"
|
||||
else
|
||||
echo "[OK] discord-dm: sent to ${LABEL} (message_id=$(printf '%s' "$BODY" | jq -r '.id // empty'))"
|
||||
exit 0
|
||||
fi
|
||||
echo "[ERROR] discord-dm: Discord returned ${HTTP:-no-response} — ${BODY}" >&2
|
||||
bash "$ROOT/.claude/scripts/log-skill-error.sh" "discord-dm" "Discord send to $LABEL failed" --context "http=${HTTP:-none} resp=${BODY:0:80}" >/dev/null 2>&1
|
||||
exit 3
|
||||
exit 0
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
# Writes: YYYY-MM-DD | MACHINE | <skill> | [<type>] <error> [ctx: <context>]
|
||||
# (newest entry inserted at the top, just under the append marker).
|
||||
#
|
||||
# Dedup: if an IDENTICAL entry (same date, machine, skill, message) already
|
||||
# exists, no new line is added — the existing line gets a " (xN)" repeat counter
|
||||
# bumped instead. Identical machine-generated failures (API retry loops) collapse
|
||||
# to one line per day; a different message/context/date is still a new entry.
|
||||
#
|
||||
# Soft-fail by design: this NEVER breaks the caller. Missing log, missing jq,
|
||||
# empty message -> prints a [WARN] to stderr and exits 0.
|
||||
set -u
|
||||
@@ -62,7 +67,7 @@ DATE="$(date -u +%F)"
|
||||
IDF="$ROOT/.claude/identity.json"
|
||||
MACHINE=""
|
||||
if command -v jq >/dev/null 2>&1 && [ -f "$IDF" ]; then
|
||||
MACHINE="$(jq -r '.machine_name // .hostname // empty' "$IDF" 2>/dev/null)"
|
||||
MACHINE="$(jq -r '.machine // .machine_name // .hostname // empty' "$IDF" 2>/dev/null)"
|
||||
fi
|
||||
[ -z "$MACHINE" ] && MACHINE="$(hostname 2>/dev/null || echo unknown)"
|
||||
|
||||
@@ -76,6 +81,34 @@ ENTRY="$DATE | $MACHINE | $SKILL | $MSG"
|
||||
|
||||
MARK="<!-- Append entries below this line -->"
|
||||
TMP="$LOG.tmp.$$"
|
||||
|
||||
# Dedup pass: an identical entry today (bare, or already counted "(xN)") gets its
|
||||
# repeat counter bumped in place instead of a duplicate line. Literal string
|
||||
# compares only — the message may contain regex metacharacters.
|
||||
DEDUP_RC=1
|
||||
awk -v entry="$ENTRY" '
|
||||
!bumped && $0 == entry { print entry " (x2)"; bumped=1; next }
|
||||
!bumped && index($0, entry " (x") == 1 {
|
||||
n = substr($0, length(entry) + 4) # text after " (x"
|
||||
if (n ~ /^[0-9]+\)$/) {
|
||||
sub(/\)$/, "", n)
|
||||
print entry " (x" n+1 ")"; bumped=1; next
|
||||
}
|
||||
}
|
||||
{ print }
|
||||
END { exit bumped ? 0 : 1 }
|
||||
' "$LOG" > "$TMP" 2>/dev/null && DEDUP_RC=0
|
||||
if [ "$DEDUP_RC" -eq 0 ]; then
|
||||
if mv "$TMP" "$LOG" 2>/dev/null; then
|
||||
echo "[OK] duplicate entry — bumped repeat counter in errorlog.md ($SKILL)"
|
||||
else
|
||||
rm -f "$TMP" 2>/dev/null
|
||||
echo "[WARN] log-skill-error: could not write $LOG" >&2
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
rm -f "$TMP" 2>/dev/null
|
||||
|
||||
if awk -v entry="$ENTRY" -v mark="$MARK" '
|
||||
{ print }
|
||||
($0==mark && !done) { print ""; print entry; done=1 }
|
||||
|
||||
@@ -35,6 +35,13 @@ if [ -z "$MSG" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Discord caps message content at 2000 chars (code 50035 on overflow). Alerts
|
||||
# are one-liners by contract — truncate rather than chunk.
|
||||
if [ "${#MSG}" -gt 1900 ]; then
|
||||
MSG="${MSG:0:1900} ...[truncated]"
|
||||
echo "[WARNING] post-bot-alert: message over 1900 chars — truncated" >&2
|
||||
fi
|
||||
|
||||
# --- channel routing ---
|
||||
# Optional 2nd arg: "dev"/"bot" keyword, a raw channel id, or omit for auto.
|
||||
# Auto: RMM/Dev-category prefixes -> #dev-alerts (private); everything else
|
||||
@@ -67,14 +74,15 @@ if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- post (jq builds JSON so the message is safely escaped) ---
|
||||
PAYLOAD="$(jq -nc --arg c "$MSG" '{content: $c}')"
|
||||
RESP="$(curl -s -m 15 -w $'\n%{http_code}' \
|
||||
# --- post (jq builds the JSON; piped to curl via STDIN, never argv — a payload
|
||||
# passed as a command-line arg on Windows goes through the argv encoding layer,
|
||||
# which mangles non-ASCII chars into invalid JSON -> Discord 50109) ---
|
||||
RESP="$(jq -nc --arg c "$MSG" '{content: $c}' | curl -s -m 15 -w $'\n%{http_code}' \
|
||||
-X POST "https://discord.com/api/v10/channels/${CHANNEL_ID}/messages" \
|
||||
-H "Authorization: Bot ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "User-Agent: ClaudeToolsBot (claudetools, 1.0)" \
|
||||
--data-binary "$PAYLOAD" 2>/dev/null)"
|
||||
--data-binary @- 2>/dev/null)"
|
||||
|
||||
HTTP="$(printf '%s' "$RESP" | tail -n1)"
|
||||
BODY="$(printf '%s' "$RESP" | sed '$d')"
|
||||
|
||||
141
.claude/scripts/ps-encoded.sh
Normal file
141
.claude/scripts/ps-encoded.sh
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env bash
|
||||
# ps-encoded.sh — dispatch PowerShell through quote-mangling layers safely.
|
||||
#
|
||||
# THE point of this helper: a PS payload that traverses CommandLineToArgvW
|
||||
# (curl.exe args, plink, ScreenConnect's command box, RMM->cmd.exe) gets its
|
||||
# embedded double-quotes stripped and its UNC backslashes halved — the single
|
||||
# most-repeated friction class in errorlog.md (ref=feedback_windows_quote_stripping,
|
||||
# 9+ hits). -EncodedCommand (UTF-16LE base64) has NO quotes, backslashes, or
|
||||
# dollar signs to mangle, so the script arrives byte-exact. Write the script
|
||||
# to a FILE (or stdin), let this helper encode + deliver it.
|
||||
#
|
||||
# Usage:
|
||||
# ps-encoded.sh encode <script.ps1|-> print the one-liner for
|
||||
# ScreenConnect / plink / any paste
|
||||
# ps-encoded.sh rmm <agent-uuid> <script.ps1|-> dispatch via GuruRMM + poll
|
||||
# [--timeout <sec>] agent-side timeout_seconds (default 120)
|
||||
# [--user-session] context: user_session (WTS-impersonated desktop user)
|
||||
# [--no-wait] dispatch only; print command id, skip polling
|
||||
# [--force] override the size refusal (see below)
|
||||
#
|
||||
# Size guards (agent chokes on ~7KB command bodies; encoding inflates ~2.67x):
|
||||
# encoded > 4000 chars -> [WARNING]; encoded > 6000 chars -> refuse unless
|
||||
# --force (split the script, or stage it on the endpoint and run the file).
|
||||
#
|
||||
# Exit codes: 0 ok; 1 usage/encode error; 2 size refusal; 3 dispatch/poll failure.
|
||||
|
||||
set -u
|
||||
ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||
_logerr() { bash "$ROOT/.claude/scripts/log-skill-error.sh" "ps-encoded" "$@" >/dev/null 2>&1 || true; }
|
||||
|
||||
usage() { sed -n '2,26p' "$0"; exit 1; }
|
||||
|
||||
read_script() { # $1 = file path or "-"
|
||||
if [ "$1" = "-" ]; then cat
|
||||
elif [ -f "$1" ]; then cat "$1"
|
||||
else echo "[ERROR] script file not found: $1" >&2; exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
encode_b64() { # stdin: UTF-8 script -> stdout: UTF-16LE base64 (no BOM, no wraps)
|
||||
iconv -f UTF-8 -t UTF-16LE | base64 -w0
|
||||
}
|
||||
|
||||
size_gate() { # $1 = encoded string, $2 = force flag (0/1)
|
||||
local n=${#1}
|
||||
if [ "$n" -gt 6000 ] && [ "${2:-0}" != "1" ]; then
|
||||
echo "[ERROR] encoded payload is ${n} chars (>6000); the agent fails on bodies this large." >&2
|
||||
echo " Split the script into sections, or stage it as a file on the endpoint" >&2
|
||||
echo " and dispatch 'powershell -File <path>'. Override with --force." >&2
|
||||
return 1
|
||||
elif [ "$n" -gt 4000 ]; then
|
||||
echo "[WARNING] encoded payload is ${n} chars — near the agent's body-size failure zone (~7KB)." >&2
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
cmd_encode() {
|
||||
local src="${1:-}"; [ -z "$src" ] && usage
|
||||
local b64
|
||||
b64="$(read_script "$src" | encode_b64)"
|
||||
[ -z "$b64" ] && { echo "[ERROR] encoding produced nothing" >&2; _logerr "encode produced empty output" --context "src=$src"; exit 1; }
|
||||
size_gate "$b64" 1 || true # encode mode: warn only, the paste target may cope
|
||||
printf 'powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand %s\n' "$b64"
|
||||
}
|
||||
|
||||
cmd_rmm() {
|
||||
local agent="${1:-}"; shift || true
|
||||
local src="${1:-}"; shift || true
|
||||
[ -z "$agent" ] || [ -z "$src" ] && usage
|
||||
local timeout=120 context="" wait=1 force=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--timeout) timeout="${2:?}"; shift 2 ;;
|
||||
--user-session) context="user_session"; shift ;;
|
||||
--no-wait) wait=0; shift ;;
|
||||
--force) force=1; shift ;;
|
||||
*) echo "[ERROR] unknown option: $1" >&2; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
local b64
|
||||
b64="$(read_script "$src" | encode_b64)"
|
||||
[ -z "$b64" ] && { echo "[ERROR] encoding produced nothing" >&2; _logerr "encode produced empty output" --context "src=$src"; exit 1; }
|
||||
size_gate "$b64" "$force" || exit 2
|
||||
|
||||
eval "$(bash "$ROOT/.claude/scripts/rmm-auth.sh")"
|
||||
if [ -z "${TOKEN:-}" ] || [ -z "${RMM:-}" ]; then
|
||||
echo "[ERROR] RMM auth failed (no TOKEN/RMM)" >&2
|
||||
_logerr "RMM auth failed via rmm-auth.sh" --context "agent=$agent"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# command_type=shell (cmd.exe): the base64 blob is a single quote-free token,
|
||||
# so no layer between here and powershell.exe can mangle it. timeout_seconds
|
||||
# is the field the agent honors — 'timeout' is silently ignored (errorlog).
|
||||
local oneliner payload resp cmd_id status
|
||||
oneliner="powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand ${b64}"
|
||||
payload="$(jq -nc --arg ct shell --arg cmd "$oneliner" --argjson to "$timeout" \
|
||||
--arg cx "$context" \
|
||||
'{command_type:$ct, command:$cmd, timeout_seconds:$to}
|
||||
+ (if $cx != "" then {context:$cx} else {} end)')"
|
||||
resp="$(printf '%s' "$payload" | curl -s -m 20 -X POST "$RMM/api/agents/$agent/command" \
|
||||
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
||||
--data-binary @-)"
|
||||
cmd_id="$(printf '%s' "$resp" | jq -r '.command_id // empty' 2>/dev/null)"
|
||||
status="$(printf '%s' "$resp" | jq -r '.status // empty' 2>/dev/null)"
|
||||
if [ -z "$cmd_id" ]; then
|
||||
echo "[ERROR] dispatch failed: $resp" >&2
|
||||
_logerr "EncodedCommand dispatch failed" --context "agent=$agent resp=${resp:0:80}"
|
||||
exit 3
|
||||
fi
|
||||
echo "[OK] dispatched command_id=$cmd_id (initial status: ${status:-unknown}, timeout_seconds=$timeout)"
|
||||
[ "$wait" = "0" ] && exit 0
|
||||
|
||||
local max_polls=$(( (timeout + 30) / 5 )) count=0 result
|
||||
while [ $count -lt $max_polls ]; do
|
||||
result="$(curl -s -m 15 "$RMM/api/commands/$cmd_id" -H "Authorization: Bearer $TOKEN")"
|
||||
status="$(printf '%s' "$result" | jq -r '.status // empty' 2>/dev/null)"
|
||||
case "$status" in
|
||||
completed|failed|cancelled|interrupted)
|
||||
echo "--- status: $status ---"
|
||||
printf '%s' "$result" | jq -r '
|
||||
"exit_code: \(.exit_code // "n/a")",
|
||||
"--- stdout ---", (.stdout // .output // ""),
|
||||
"--- stderr ---", (.stderr // "")' 2>/dev/null || printf '%s\n' "$result"
|
||||
[ "$status" = "completed" ] && exit 0 || exit 3 ;;
|
||||
running|pending) count=$((count+1)); sleep 5 ;;
|
||||
*) echo "[ERROR] empty/unknown status — response: $result" >&2
|
||||
_logerr "poll returned empty status" --context "cmd=$cmd_id agent=$agent"
|
||||
exit 3 ;;
|
||||
esac
|
||||
done
|
||||
echo "[WARNING] poll timeout after $((max_polls*5))s — command $cmd_id may still be running (last: $status)"
|
||||
exit 3
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
encode) shift; cmd_encode "$@" ;;
|
||||
rmm) shift; cmd_rmm "$@" ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user