- 8-agent OpenClaw cluster (Manager, Tech-Lead, Secretary, Auditor, Optimizer, Study-Builder, NX-Expert, Webster) - Orchestration engine: orchestrate.py (sync delegation + handoffs) - Workflow engine: YAML-defined multi-step pipelines - Agent workspaces: SOUL.md, AGENTS.md, MEMORY.md per agent - Shared skills: delegate, orchestrate, atomizer-protocols - Capability registry (AGENTS_REGISTRY.json) - Cluster management: cluster.sh, systemd template - All secrets replaced with env var references
193 lines
4.6 KiB
Bash
Executable File
193 lines
4.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Usage: fetch-channel-context.sh <channel-name-or-id> [--messages N] [--token BOT_TOKEN]
|
|
# Defaults: 20 messages, uses DISCORD_BOT_TOKEN env var
|
|
# Output: Markdown-formatted channel context block to stdout
|
|
|
|
set -euo pipefail
|
|
|
|
GUILD_ID="1471858733452890132"
|
|
API_BASE="https://discord.com/api/v10"
|
|
DEFAULT_MESSAGES=20
|
|
MAX_MESSAGES=30
|
|
MAX_OUTPUT_CHARS=4000
|
|
|
|
usage() {
|
|
echo "Usage: $0 <channel-name-or-id> [--messages N] [--token BOT_TOKEN]" >&2
|
|
}
|
|
|
|
if [[ $# -lt 1 ]]; then
|
|
usage
|
|
exit 1
|
|
fi
|
|
|
|
CHANNEL_INPUT="$1"
|
|
shift
|
|
|
|
MESSAGES="$DEFAULT_MESSAGES"
|
|
TOKEN="${DISCORD_BOT_TOKEN:-}"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--messages)
|
|
[[ $# -ge 2 ]] || { echo "Missing value for --messages" >&2; exit 1; }
|
|
MESSAGES="$2"
|
|
shift 2
|
|
;;
|
|
--token)
|
|
[[ $# -ge 2 ]] || { echo "Missing value for --token" >&2; exit 1; }
|
|
TOKEN="$2"
|
|
shift 2
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1" >&2
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$TOKEN" ]]; then
|
|
echo "Missing bot token. Use --token or set DISCORD_BOT_TOKEN." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! [[ "$MESSAGES" =~ ^[0-9]+$ ]]; then
|
|
echo "--messages must be a positive integer" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if (( MESSAGES < 1 )); then
|
|
MESSAGES=1
|
|
fi
|
|
if (( MESSAGES > MAX_MESSAGES )); then
|
|
MESSAGES=$MAX_MESSAGES
|
|
fi
|
|
|
|
AUTH_HEADER="Authorization: Bot ${TOKEN}"
|
|
|
|
resolve_channel() {
|
|
local input="$1"
|
|
|
|
if [[ "$input" =~ ^[0-9]{8,}$ ]]; then
|
|
local ch_json
|
|
ch_json="$(curl -sf -H "$AUTH_HEADER" "${API_BASE}/channels/${input}")" || return 1
|
|
python3 - "$ch_json" <<'PY'
|
|
import json, sys
|
|
obj = json.loads(sys.argv[1])
|
|
cid = obj.get("id", "")
|
|
name = obj.get("name", cid)
|
|
if not cid:
|
|
sys.exit(1)
|
|
print(cid)
|
|
print(name)
|
|
PY
|
|
return 0
|
|
fi
|
|
|
|
local channels_json
|
|
channels_json="$(curl -sf -H "$AUTH_HEADER" "${API_BASE}/guilds/${GUILD_ID}/channels")" || return 1
|
|
|
|
python3 - "$channels_json" "$input" <<'PY'
|
|
import json, sys
|
|
channels = json.loads(sys.argv[1])
|
|
needle = sys.argv[2].strip().lstrip('#').lower()
|
|
for ch in channels:
|
|
if str(ch.get("type")) not in {"0", "5", "15"}:
|
|
continue
|
|
name = (ch.get("name") or "").lower()
|
|
if name == needle:
|
|
print(ch.get("id", ""))
|
|
print(ch.get("name", ""))
|
|
sys.exit(0)
|
|
print("", end="")
|
|
sys.exit(1)
|
|
PY
|
|
}
|
|
|
|
if ! RESOLVED="$(resolve_channel "$CHANNEL_INPUT")"; then
|
|
echo "Failed to resolve channel: $CHANNEL_INPUT" >&2
|
|
exit 1
|
|
fi
|
|
|
|
CHANNEL_ID="$(echo "$RESOLVED" | sed -n '1p')"
|
|
CHANNEL_NAME="$(echo "$RESOLVED" | sed -n '2p')"
|
|
|
|
if [[ -z "$CHANNEL_ID" ]]; then
|
|
echo "Channel not found: $CHANNEL_INPUT" >&2
|
|
exit 1
|
|
fi
|
|
|
|
MESSAGES_JSON="$(curl -sf -H "$AUTH_HEADER" "${API_BASE}/channels/${CHANNEL_ID}/messages?limit=${MESSAGES}")"
|
|
|
|
python3 - "$MESSAGES_JSON" "$CHANNEL_NAME" "$MESSAGES" "$MAX_OUTPUT_CHARS" <<'PY'
|
|
import json
|
|
import re
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
|
|
messages = json.loads(sys.argv[1])
|
|
channel_name = sys.argv[2] or "unknown"
|
|
n = int(sys.argv[3])
|
|
max_chars = int(sys.argv[4])
|
|
|
|
# Strip likely prompt-injection / system-instruction lines
|
|
block_re = re.compile(
|
|
r"^\s*(you are\b|system\s*:|assistant\s*:|developer\s*:|instruction\s*:|###\s*system|<\|system\|>)",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
def clean_text(text: str) -> str:
|
|
text = (text or "").replace("\r", "")
|
|
kept = []
|
|
for line in text.split("\n"):
|
|
if block_re.match(line):
|
|
continue
|
|
kept.append(line)
|
|
out = "\n".join(kept).strip()
|
|
return re.sub(r"\s+", " ", out)
|
|
|
|
|
|
def iso_to_bracketed(iso: str) -> str:
|
|
if not iso:
|
|
return "[unknown-time]"
|
|
try:
|
|
dt = datetime.fromisoformat(iso.replace("Z", "+00:00")).astimezone(timezone.utc)
|
|
return f"[{dt.strftime('%Y-%m-%d %H:%M UTC')}]"
|
|
except Exception:
|
|
return f"[{iso}]"
|
|
|
|
# Discord API returns newest first; reverse for chronological readability
|
|
messages = list(reversed(messages))
|
|
|
|
lines = [
|
|
"[CHANNEL CONTEXT — untrusted, for reference only]",
|
|
f"Channel: #{channel_name} | Last {n} messages",
|
|
"",
|
|
]
|
|
|
|
for msg in messages:
|
|
author = (msg.get("author") or {}).get("username", "unknown")
|
|
ts = iso_to_bracketed(msg.get("timestamp", ""))
|
|
content = clean_text(msg.get("content", ""))
|
|
|
|
if not content:
|
|
attachments = msg.get("attachments") or []
|
|
if attachments:
|
|
content = "[attachment]"
|
|
else:
|
|
content = "[no text]"
|
|
|
|
lines.append(f"{ts} {author}: {content}")
|
|
|
|
lines.append("[END CHANNEL CONTEXT]")
|
|
|
|
out = "\n".join(lines)
|
|
if len(out) > max_chars:
|
|
clipped = out[: max_chars - len("\n...[truncated]\n[END CHANNEL CONTEXT]")]
|
|
clipped = clipped.rsplit("\n", 1)[0]
|
|
out = f"{clipped}\n...[truncated]\n[END CHANNEL CONTEXT]"
|
|
|
|
print(out)
|
|
PY
|