u-foo 1.0.0
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/LICENSE +35 -0
- package/README.md +163 -0
- package/README.zh-CN.md +163 -0
- package/bin/uclaude +65 -0
- package/bin/ucodex +65 -0
- package/bin/ufoo +93 -0
- package/bin/ufoo.js +35 -0
- package/modules/AGENTS.template.md +87 -0
- package/modules/bus/README.md +132 -0
- package/modules/bus/SKILLS/ubus/SKILL.md +209 -0
- package/modules/bus/scripts/bus-alert.sh +185 -0
- package/modules/bus/scripts/bus-listen.sh +117 -0
- package/modules/context/ASSUMPTIONS.md +7 -0
- package/modules/context/CONSTRAINTS.md +7 -0
- package/modules/context/CONTEXT-STRUCTURE.md +49 -0
- package/modules/context/DECISION-PROTOCOL.md +62 -0
- package/modules/context/HANDOFF.md +33 -0
- package/modules/context/README.md +82 -0
- package/modules/context/RULES.md +15 -0
- package/modules/context/SKILLS/README.md +14 -0
- package/modules/context/SKILLS/uctx/SKILL.md +91 -0
- package/modules/context/SYSTEM.md +18 -0
- package/modules/context/TEMPLATES/assumptions.md +4 -0
- package/modules/context/TEMPLATES/constraints.md +4 -0
- package/modules/context/TEMPLATES/decision.md +16 -0
- package/modules/context/TEMPLATES/project-context-readme.md +6 -0
- package/modules/context/TEMPLATES/system.md +3 -0
- package/modules/context/TEMPLATES/terminology.md +4 -0
- package/modules/context/TERMINOLOGY.md +10 -0
- package/modules/resources/ICONS/README.md +12 -0
- package/modules/resources/ICONS/libraries/README.md +17 -0
- package/modules/resources/ICONS/libraries/heroicons/LICENSE +22 -0
- package/modules/resources/ICONS/libraries/heroicons/README.md +15 -0
- package/modules/resources/ICONS/libraries/heroicons/arrow-right.svg +4 -0
- package/modules/resources/ICONS/libraries/heroicons/check.svg +4 -0
- package/modules/resources/ICONS/libraries/heroicons/chevron-down.svg +4 -0
- package/modules/resources/ICONS/libraries/heroicons/cog-6-tooth.svg +5 -0
- package/modules/resources/ICONS/libraries/heroicons/magnifying-glass.svg +4 -0
- package/modules/resources/ICONS/libraries/heroicons/x-mark.svg +4 -0
- package/modules/resources/ICONS/libraries/lucide/LICENSE +40 -0
- package/modules/resources/ICONS/libraries/lucide/README.md +15 -0
- package/modules/resources/ICONS/libraries/lucide/arrow-right.svg +15 -0
- package/modules/resources/ICONS/libraries/lucide/check.svg +14 -0
- package/modules/resources/ICONS/libraries/lucide/chevron-down.svg +14 -0
- package/modules/resources/ICONS/libraries/lucide/search.svg +15 -0
- package/modules/resources/ICONS/libraries/lucide/settings.svg +15 -0
- package/modules/resources/ICONS/libraries/lucide/x.svg +15 -0
- package/modules/resources/ICONS/rules.md +7 -0
- package/modules/resources/README.md +9 -0
- package/modules/resources/UI/ANTI-PATTERNS.md +6 -0
- package/modules/resources/UI/TONE.md +6 -0
- package/package.json +40 -0
- package/scripts/banner.sh +89 -0
- package/scripts/bus-alert.sh +6 -0
- package/scripts/bus-autotrigger.sh +6 -0
- package/scripts/bus-daemon.sh +231 -0
- package/scripts/bus-inject.sh +144 -0
- package/scripts/bus-listen.sh +6 -0
- package/scripts/bus.sh +984 -0
- package/scripts/context-decisions.sh +167 -0
- package/scripts/context-doctor.sh +72 -0
- package/scripts/context-lint.sh +110 -0
- package/scripts/doctor.sh +22 -0
- package/scripts/init.sh +247 -0
- package/scripts/skills.sh +113 -0
- package/scripts/status.sh +125 -0
- package/src/agent/cliRunner.js +190 -0
- package/src/agent/internalRunner.js +212 -0
- package/src/agent/normalizeOutput.js +41 -0
- package/src/agent/ufooAgent.js +222 -0
- package/src/chat/index.js +1603 -0
- package/src/cli.js +349 -0
- package/src/config.js +37 -0
- package/src/daemon/index.js +501 -0
- package/src/daemon/ops.js +120 -0
- package/src/daemon/run.js +41 -0
- package/src/daemon/status.js +78 -0
package/scripts/bus.sh
ADDED
|
@@ -0,0 +1,984 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# bus: Project-level Agent event bus
|
|
5
|
+
# Independent module, stored in .ufoo/bus/
|
|
6
|
+
#
|
|
7
|
+
# Usage: bus <command> [options]
|
|
8
|
+
#
|
|
9
|
+
# Commands:
|
|
10
|
+
# init Initialize event bus
|
|
11
|
+
# join [session-id] Join bus (register current instance)
|
|
12
|
+
# send <target> <message> Send targeted message
|
|
13
|
+
# broadcast <message> Broadcast message
|
|
14
|
+
# check Check pending events
|
|
15
|
+
# status View bus status
|
|
16
|
+
# consume Consume events
|
|
17
|
+
# leave Leave bus
|
|
18
|
+
|
|
19
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
|
+
BUS_DIR=".ufoo/bus"
|
|
21
|
+
DATE_FORMAT="%Y-%m-%dT%H:%M:%S.000Z"
|
|
22
|
+
|
|
23
|
+
# Colors
|
|
24
|
+
RED='\033[0;31m'
|
|
25
|
+
GREEN='\033[0;32m'
|
|
26
|
+
YELLOW='\033[0;33m'
|
|
27
|
+
BLUE='\033[0;34m'
|
|
28
|
+
CYAN='\033[0;36m'
|
|
29
|
+
NC='\033[0m'
|
|
30
|
+
|
|
31
|
+
log_info() { echo -e "${BLUE}[bus]${NC} $*"; }
|
|
32
|
+
log_ok() { echo -e "${GREEN}[bus]${NC} $*"; }
|
|
33
|
+
log_warn() { echo -e "${YELLOW}[bus]${NC} $*"; }
|
|
34
|
+
log_error() { echo -e "${RED}[bus]${NC} $*" >&2; }
|
|
35
|
+
|
|
36
|
+
# ============================================================================
|
|
37
|
+
# Utility functions
|
|
38
|
+
# ============================================================================
|
|
39
|
+
|
|
40
|
+
get_timestamp() {
|
|
41
|
+
date -u +"$DATE_FORMAT"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get_date() {
|
|
45
|
+
date -u +"%Y-%m-%d"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
generate_instance_id() {
|
|
49
|
+
echo "$(date +%s%N | shasum | head -c 8)"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
ensure_bus() {
|
|
53
|
+
if [[ ! -d "$BUS_DIR" ]]; then
|
|
54
|
+
log_error "Event bus not initialized. Please run: bus init or /uinit"
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
subscriber_to_safe_name() {
|
|
60
|
+
echo "${1//:/_}"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get_next_seq() {
|
|
64
|
+
local today_file="$BUS_DIR/events/$(get_date).jsonl"
|
|
65
|
+
if [[ -f "$today_file" ]]; then
|
|
66
|
+
local last_seq
|
|
67
|
+
last_seq=$(tail -1 "$today_file" 2>/dev/null | jq -r '.seq // 0' 2>/dev/null || echo "0")
|
|
68
|
+
echo $((last_seq + 1))
|
|
69
|
+
else
|
|
70
|
+
local max_seq=0
|
|
71
|
+
shopt -s nullglob
|
|
72
|
+
for f in "$BUS_DIR/events"/*.jsonl; do
|
|
73
|
+
if [[ -f "$f" ]]; then
|
|
74
|
+
local file_max
|
|
75
|
+
file_max=$(tail -1 "$f" 2>/dev/null | jq -r '.seq // 0' 2>/dev/null || echo "0")
|
|
76
|
+
if [[ $file_max -gt $max_seq ]]; then
|
|
77
|
+
max_seq=$file_max
|
|
78
|
+
fi
|
|
79
|
+
fi
|
|
80
|
+
done
|
|
81
|
+
echo $((max_seq + 1))
|
|
82
|
+
fi
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Best-effort check for currently running process.
|
|
86
|
+
is_pid_alive() {
|
|
87
|
+
local pid="${1:-0}"
|
|
88
|
+
if [[ -z "$pid" || "$pid" == "0" ]]; then
|
|
89
|
+
return 1
|
|
90
|
+
fi
|
|
91
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
92
|
+
return 0
|
|
93
|
+
fi
|
|
94
|
+
return 1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Resolve nickname to subscriber ID
|
|
98
|
+
resolve_nickname() {
|
|
99
|
+
local nickname="$1"
|
|
100
|
+
ensure_bus
|
|
101
|
+
jq -r --arg nick "$nickname" \
|
|
102
|
+
'.subscribers | to_entries[] | select(.value.nickname == $nick) | .key' \
|
|
103
|
+
"$BUS_DIR/bus.json" | head -1
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Check if target matches subscriber
|
|
107
|
+
target_matches() {
|
|
108
|
+
local target="$1"
|
|
109
|
+
local subscriber="$2"
|
|
110
|
+
|
|
111
|
+
# Priority 1: Wildcard/empty
|
|
112
|
+
[[ -z "$target" || "$target" == "*" ]] && return 0
|
|
113
|
+
|
|
114
|
+
# Priority 2: Exact subscriber ID match
|
|
115
|
+
[[ "$target" == "$subscriber" ]] && return 0
|
|
116
|
+
|
|
117
|
+
# Priority 3: Nickname match (resolve nickname to real ID)
|
|
118
|
+
if [[ ! "$target" =~ : ]]; then
|
|
119
|
+
local resolved
|
|
120
|
+
resolved=$(resolve_nickname "$target")
|
|
121
|
+
if [[ -n "$resolved" ]]; then
|
|
122
|
+
[[ "$resolved" == "$subscriber" ]] && return 0
|
|
123
|
+
fi
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# Priority 4: Agent type match
|
|
127
|
+
local target_type="${target%%:*}"
|
|
128
|
+
local target_instance="${target#*:}"
|
|
129
|
+
local sub_type="${subscriber%%:*}"
|
|
130
|
+
|
|
131
|
+
[[ "$target" == "$target_type" && "$target_type" == "$sub_type" ]] && return 0
|
|
132
|
+
[[ "$target_instance" == "*" && "$target_type" == "$sub_type" ]] && return 0
|
|
133
|
+
|
|
134
|
+
return 1
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# ============================================================================
|
|
138
|
+
# Command: init
|
|
139
|
+
# ============================================================================
|
|
140
|
+
|
|
141
|
+
cmd_init() {
|
|
142
|
+
if [[ -d "$BUS_DIR" ]]; then
|
|
143
|
+
log_warn "Event bus already exists"
|
|
144
|
+
return 0
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
log_info "Initializing event bus..."
|
|
148
|
+
|
|
149
|
+
mkdir -p "$BUS_DIR"/{events,offsets,queues}
|
|
150
|
+
|
|
151
|
+
local project_name
|
|
152
|
+
project_name=$(basename "$(pwd)")
|
|
153
|
+
|
|
154
|
+
cat > "$BUS_DIR/bus.json" << EOF
|
|
155
|
+
{
|
|
156
|
+
"bus_id": "${project_name}-bus",
|
|
157
|
+
"created_at": "$(get_timestamp)",
|
|
158
|
+
"subscribers": {},
|
|
159
|
+
"agent_types": {},
|
|
160
|
+
"config": {
|
|
161
|
+
"poll_interval_ms": 3000,
|
|
162
|
+
"heartbeat_timeout_ms": 30000
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
EOF
|
|
166
|
+
|
|
167
|
+
# Record initialization event
|
|
168
|
+
local today_file="$BUS_DIR/events/$(get_date).jsonl"
|
|
169
|
+
echo "{\"seq\":1,\"ts\":\"$(get_timestamp)\",\"type\":\"system\",\"event\":\"bus_created\",\"publisher\":\"system\",\"data\":{}}" >> "$today_file"
|
|
170
|
+
|
|
171
|
+
log_ok "Event bus initialized: $BUS_DIR"
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# ============================================================================
|
|
175
|
+
# Command: join (join bus)
|
|
176
|
+
# ============================================================================
|
|
177
|
+
|
|
178
|
+
cmd_join() {
|
|
179
|
+
local session_id="${1:-}"
|
|
180
|
+
local agent_type="${2:-}"
|
|
181
|
+
local nickname="${3:-}"
|
|
182
|
+
|
|
183
|
+
ensure_bus
|
|
184
|
+
|
|
185
|
+
# If no session_id provided, try to get from env or generate
|
|
186
|
+
if [[ -z "$session_id" ]]; then
|
|
187
|
+
session_id="${CLAUDE_SESSION_ID:-${CODEX_SESSION_ID:-$(generate_instance_id)}}"
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
# If no agent_type provided, auto-detect from env
|
|
191
|
+
if [[ -z "$agent_type" ]]; then
|
|
192
|
+
if [[ -n "${CODEX_SESSION_ID:-}" ]]; then
|
|
193
|
+
agent_type="codex"
|
|
194
|
+
else
|
|
195
|
+
agent_type="claude-code"
|
|
196
|
+
fi
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
local subscriber="${agent_type}:${session_id}"
|
|
200
|
+
local safe_name
|
|
201
|
+
safe_name=$(subscriber_to_safe_name "$subscriber")
|
|
202
|
+
|
|
203
|
+
# Check if subscriber already exists (rejoin scenario)
|
|
204
|
+
local existing_nickname
|
|
205
|
+
existing_nickname=$(jq -r --arg id "$subscriber" '.subscribers[$id].nickname // ""' "$BUS_DIR/bus.json" 2>/dev/null)
|
|
206
|
+
|
|
207
|
+
# Handle nickname: reuse existing, auto-generate, or use provided
|
|
208
|
+
if [[ -n "$existing_nickname" ]]; then
|
|
209
|
+
# Subscriber already exists
|
|
210
|
+
if [[ -z "$nickname" ]]; then
|
|
211
|
+
# No nickname provided: reuse existing
|
|
212
|
+
nickname="$existing_nickname"
|
|
213
|
+
elif [[ "$nickname" != "$existing_nickname" ]]; then
|
|
214
|
+
# Different nickname provided: error (use cmd_rename instead)
|
|
215
|
+
log_error "Subscriber $subscriber already exists with nickname '$existing_nickname'"
|
|
216
|
+
log_error "To change nickname, use: bus rename $subscriber '$nickname'"
|
|
217
|
+
exit 1
|
|
218
|
+
fi
|
|
219
|
+
# else: same nickname provided, continue with rejoin
|
|
220
|
+
else
|
|
221
|
+
# New subscriber: auto-generate or validate provided nickname
|
|
222
|
+
if [[ -z "$nickname" ]]; then
|
|
223
|
+
local count
|
|
224
|
+
count=$(jq -r --arg type "$agent_type" \
|
|
225
|
+
'.subscribers | to_entries[] |
|
|
226
|
+
select(.value.agent_type == $type) | .key' \
|
|
227
|
+
"$BUS_DIR/bus.json" 2>/dev/null | wc -l | tr -d ' ')
|
|
228
|
+
nickname="${agent_type}-$((count + 1))"
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
# Check nickname uniqueness for new subscribers
|
|
232
|
+
local existing
|
|
233
|
+
existing=$(resolve_nickname "$nickname" 2>/dev/null || echo "")
|
|
234
|
+
if [[ -n "$existing" && "$existing" != "$subscriber" ]]; then
|
|
235
|
+
log_error "Nickname '$nickname' already in use by $existing"
|
|
236
|
+
exit 1
|
|
237
|
+
fi
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
log_info "Joining event bus: $subscriber (nickname: $nickname)"
|
|
241
|
+
|
|
242
|
+
# Set terminal window title with session id and nickname
|
|
243
|
+
if [[ -n "$nickname" ]]; then
|
|
244
|
+
local title="[bus:${session_id}] ${nickname}"
|
|
245
|
+
else
|
|
246
|
+
local title="[bus:${session_id}]"
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
# Optional: set terminal title (for human identification; disabled by default to avoid stdout pollution)
|
|
250
|
+
if [[ "${AI_BUS_SET_TITLE:-0}" == "1" ]]; then
|
|
251
|
+
echo -ne "\\033]0;${title}\\007"
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
# Update bus.json
|
|
255
|
+
local tmp_file
|
|
256
|
+
tmp_file=$(mktemp)
|
|
257
|
+
|
|
258
|
+
local tmux_pane="${TMUX_PANE:-}"
|
|
259
|
+
local tmux_session="${TMUX_SESSION:-}"
|
|
260
|
+
local tty_path=""
|
|
261
|
+
tty_path="$(tty 2>/dev/null || true)"
|
|
262
|
+
if [[ "$tty_path" == "not a tty" ]]; then
|
|
263
|
+
tty_path=""
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
# Clean up old subscribers on same tty (avoid duplicate subscriptions)
|
|
267
|
+
if [[ -n "$tty_path" ]]; then
|
|
268
|
+
for old_queue in "$BUS_DIR/queues"/*/tty; do
|
|
269
|
+
if [[ -f "$old_queue" ]]; then
|
|
270
|
+
old_tty=$(cat "$old_queue")
|
|
271
|
+
if [[ "$old_tty" == "$tty_path" ]]; then
|
|
272
|
+
old_dir=$(dirname "$old_queue")
|
|
273
|
+
old_safe_name=$(basename "$old_dir")
|
|
274
|
+
old_subscriber="${old_safe_name/_/:}"
|
|
275
|
+
if [[ "$old_subscriber" != "$subscriber" ]]; then
|
|
276
|
+
log_info "Cleaning up old subscription on same tty: $old_subscriber"
|
|
277
|
+
rm -rf "$old_dir"
|
|
278
|
+
rm -f "$BUS_DIR/offsets/${old_safe_name}.offset"
|
|
279
|
+
# Remove from bus.json
|
|
280
|
+
jq --arg name "$old_subscriber" 'del(.subscribers[$name])' "$BUS_DIR/bus.json" > "$tmp_file" && mv "$tmp_file" "$BUS_DIR/bus.json"
|
|
281
|
+
tmp_file=$(mktemp)
|
|
282
|
+
fi
|
|
283
|
+
fi
|
|
284
|
+
fi
|
|
285
|
+
done
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
jq --arg name "$subscriber" \
|
|
289
|
+
--arg agent_type "$agent_type" \
|
|
290
|
+
--arg instance_id "$session_id" \
|
|
291
|
+
--arg nickname "$nickname" \
|
|
292
|
+
--arg tmux_pane "$tmux_pane" \
|
|
293
|
+
--arg tmux_session "$tmux_session" \
|
|
294
|
+
--arg tty "$tty_path" \
|
|
295
|
+
--arg ts "$(get_timestamp)" \
|
|
296
|
+
--arg pid "${UFOO_PARENT_PID:-$PPID}" \
|
|
297
|
+
--arg cwd "$(pwd)" \
|
|
298
|
+
'
|
|
299
|
+
.subscribers[$name] = {
|
|
300
|
+
"agent_type": $agent_type,
|
|
301
|
+
"instance_id": $instance_id,
|
|
302
|
+
"nickname": $nickname,
|
|
303
|
+
"tmux_pane": (if $tmux_pane != "" then $tmux_pane else null end),
|
|
304
|
+
"tmux_session": (if $tmux_session != "" then $tmux_session else null end),
|
|
305
|
+
"tty": (if $tty != "" then $tty else null end),
|
|
306
|
+
"pid": ($pid | tonumber),
|
|
307
|
+
"joined_at": $ts,
|
|
308
|
+
"status": "active",
|
|
309
|
+
"last_heartbeat": $ts
|
|
310
|
+
}
|
|
311
|
+
|
|
|
312
|
+
.agent_types[$agent_type].instances = (
|
|
313
|
+
(.agent_types[$agent_type].instances // []) + [$instance_id] | unique
|
|
314
|
+
)
|
|
315
|
+
|
|
|
316
|
+
.agent_types[$agent_type].active_count = (
|
|
317
|
+
.subscribers | to_entries | map(select(.value.agent_type == $agent_type and .value.status == "active")) | length
|
|
318
|
+
)
|
|
319
|
+
' "$BUS_DIR/bus.json" > "$tmp_file"
|
|
320
|
+
|
|
321
|
+
mv "$tmp_file" "$BUS_DIR/bus.json"
|
|
322
|
+
|
|
323
|
+
# Create offset file
|
|
324
|
+
cat > "$BUS_DIR/offsets/${safe_name}.offset" << EOF
|
|
325
|
+
{
|
|
326
|
+
"subscriber": "$subscriber",
|
|
327
|
+
"current_seq": 0,
|
|
328
|
+
"last_consumed_at": "$(get_timestamp)"
|
|
329
|
+
}
|
|
330
|
+
EOF
|
|
331
|
+
|
|
332
|
+
# Create queue directory
|
|
333
|
+
mkdir -p "$BUS_DIR/queues/${safe_name}"
|
|
334
|
+
# Best-effort: persist tty for other scripts (e.g. injection) without parsing bus.json
|
|
335
|
+
if [[ -n "$tty_path" ]]; then
|
|
336
|
+
echo "$tty_path" > "$BUS_DIR/queues/${safe_name}/tty"
|
|
337
|
+
fi
|
|
338
|
+
|
|
339
|
+
# Record tty device (for later inject targeting)
|
|
340
|
+
local current_tty
|
|
341
|
+
current_tty=$(tty 2>/dev/null || echo "")
|
|
342
|
+
if [[ -n "$current_tty" && "$current_tty" != "not a tty" ]]; then
|
|
343
|
+
echo "$current_tty" > "$BUS_DIR/queues/${safe_name}/tty"
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
# Publish join event
|
|
347
|
+
local seq
|
|
348
|
+
seq=$(get_next_seq)
|
|
349
|
+
local today_file="$BUS_DIR/events/$(get_date).jsonl"
|
|
350
|
+
echo "{\"seq\":$seq,\"ts\":\"$(get_timestamp)\",\"type\":\"system\",\"event\":\"agent_joined\",\"publisher\":\"$subscriber\",\"data\":{\"agent_type\":\"$agent_type\",\"instance_id\":\"$session_id\"}}" >> "$today_file"
|
|
351
|
+
|
|
352
|
+
log_ok "Joined event bus"
|
|
353
|
+
echo ""
|
|
354
|
+
echo -e "${CYAN}My identity: $subscriber${NC}"
|
|
355
|
+
echo ""
|
|
356
|
+
|
|
357
|
+
# Check pending events
|
|
358
|
+
cmd_check "$subscriber"
|
|
359
|
+
|
|
360
|
+
# Output subscriber ID
|
|
361
|
+
echo "$subscriber"
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
# ============================================================================
|
|
365
|
+
# Command: check (check pending events)
|
|
366
|
+
# ============================================================================
|
|
367
|
+
|
|
368
|
+
cmd_check() {
|
|
369
|
+
local subscriber="${1:-}"
|
|
370
|
+
local auto_ack="${2:-}"
|
|
371
|
+
|
|
372
|
+
if [[ -z "$subscriber" ]]; then
|
|
373
|
+
log_error "Usage: bus check <subscriber-id> [--ack]"
|
|
374
|
+
exit 1
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
ensure_bus
|
|
378
|
+
|
|
379
|
+
local safe_name
|
|
380
|
+
safe_name=$(subscriber_to_safe_name "$subscriber")
|
|
381
|
+
local queue_file="$BUS_DIR/queues/${safe_name}/pending.jsonl"
|
|
382
|
+
|
|
383
|
+
if [[ -f "$queue_file" && -s "$queue_file" ]]; then
|
|
384
|
+
local count
|
|
385
|
+
count=$(wc -l < "$queue_file" | tr -d ' ')
|
|
386
|
+
log_warn "You have $count pending event(s):"
|
|
387
|
+
echo ""
|
|
388
|
+
while IFS= read -r event; do
|
|
389
|
+
local publisher type event_name data
|
|
390
|
+
publisher=$(echo "$event" | jq -r '.publisher')
|
|
391
|
+
type=$(echo "$event" | jq -r '.type')
|
|
392
|
+
event_name=$(echo "$event" | jq -r '.event')
|
|
393
|
+
data=$(echo "$event" | jq -c '.data')
|
|
394
|
+
echo -e " ${YELLOW}@you${NC} from ${CYAN}$publisher${NC}"
|
|
395
|
+
echo -e " Type: $type/$event_name"
|
|
396
|
+
echo -e " Content: $data"
|
|
397
|
+
echo ""
|
|
398
|
+
done < "$queue_file"
|
|
399
|
+
|
|
400
|
+
# Auto-ack if requested, or show hint
|
|
401
|
+
if [[ "$auto_ack" == "--ack" ]]; then
|
|
402
|
+
: > "$queue_file"
|
|
403
|
+
log_ok "Messages acknowledged and cleared"
|
|
404
|
+
else
|
|
405
|
+
echo -e "${CYAN}After handling, run: ufoo bus ack $subscriber${NC}"
|
|
406
|
+
fi
|
|
407
|
+
else
|
|
408
|
+
log_ok "No pending events"
|
|
409
|
+
fi
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
# ============================================================================
|
|
413
|
+
# Command: ack (acknowledge/clear pending messages)
|
|
414
|
+
# ============================================================================
|
|
415
|
+
|
|
416
|
+
cmd_ack() {
|
|
417
|
+
local subscriber="${1:-}"
|
|
418
|
+
|
|
419
|
+
if [[ -z "$subscriber" ]]; then
|
|
420
|
+
log_error "Usage: bus ack <subscriber-id>"
|
|
421
|
+
exit 1
|
|
422
|
+
fi
|
|
423
|
+
|
|
424
|
+
ensure_bus
|
|
425
|
+
|
|
426
|
+
local safe_name
|
|
427
|
+
safe_name=$(subscriber_to_safe_name "$subscriber")
|
|
428
|
+
local queue_file="$BUS_DIR/queues/${safe_name}/pending.jsonl"
|
|
429
|
+
|
|
430
|
+
if [[ -f "$queue_file" && -s "$queue_file" ]]; then
|
|
431
|
+
local count
|
|
432
|
+
count=$(wc -l < "$queue_file" | tr -d ' ')
|
|
433
|
+
# Clear the queue
|
|
434
|
+
: > "$queue_file"
|
|
435
|
+
log_ok "Acknowledged and cleared $count message(s)"
|
|
436
|
+
else
|
|
437
|
+
log_ok "No pending messages to acknowledge"
|
|
438
|
+
fi
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
# ============================================================================
|
|
442
|
+
# Command: send (send targeted message)
|
|
443
|
+
# ============================================================================
|
|
444
|
+
|
|
445
|
+
cmd_send() {
|
|
446
|
+
local target="${1:-}"
|
|
447
|
+
local message="${2:-}"
|
|
448
|
+
|
|
449
|
+
# Auto-detect publisher: prefer env var, otherwise build from session ID
|
|
450
|
+
local publisher="${AI_BUS_PUBLISHER:-}"
|
|
451
|
+
if [[ -z "$publisher" ]]; then
|
|
452
|
+
if [[ -n "${CODEX_SESSION_ID:-}" ]]; then
|
|
453
|
+
publisher="codex:${CODEX_SESSION_ID}"
|
|
454
|
+
elif [[ -n "${CLAUDE_SESSION_ID:-}" ]]; then
|
|
455
|
+
publisher="claude-code:${CLAUDE_SESSION_ID}"
|
|
456
|
+
else
|
|
457
|
+
publisher="unknown"
|
|
458
|
+
fi
|
|
459
|
+
fi
|
|
460
|
+
|
|
461
|
+
if [[ -z "$target" || -z "$message" ]]; then
|
|
462
|
+
log_error "Usage: context-bus send <target> <message>"
|
|
463
|
+
log_error "Example: context-bus send claude-code:abc123 'Please help me review'"
|
|
464
|
+
exit 1
|
|
465
|
+
fi
|
|
466
|
+
|
|
467
|
+
ensure_bus
|
|
468
|
+
|
|
469
|
+
local seq
|
|
470
|
+
seq=$(get_next_seq)
|
|
471
|
+
local today_file="$BUS_DIR/events/$(get_date).jsonl"
|
|
472
|
+
|
|
473
|
+
# Build event
|
|
474
|
+
local event_json
|
|
475
|
+
event_json=$(jq -cn \
|
|
476
|
+
--argjson seq "$seq" \
|
|
477
|
+
--arg ts "$(get_timestamp)" \
|
|
478
|
+
--arg publisher "$publisher" \
|
|
479
|
+
--arg target "$target" \
|
|
480
|
+
--arg message "$message" \
|
|
481
|
+
'{
|
|
482
|
+
seq: $seq,
|
|
483
|
+
ts: $ts,
|
|
484
|
+
type: "message",
|
|
485
|
+
event: "targeted",
|
|
486
|
+
publisher: $publisher,
|
|
487
|
+
target: $target,
|
|
488
|
+
data: { message: $message }
|
|
489
|
+
}')
|
|
490
|
+
|
|
491
|
+
echo "$event_json" >> "$today_file"
|
|
492
|
+
|
|
493
|
+
# Write to target queue
|
|
494
|
+
local matching_subscribers
|
|
495
|
+
matching_subscribers=$(jq -r '.subscribers | keys[]' "$BUS_DIR/bus.json")
|
|
496
|
+
|
|
497
|
+
for sub in $matching_subscribers; do
|
|
498
|
+
if target_matches "$target" "$sub"; then
|
|
499
|
+
local safe_name
|
|
500
|
+
safe_name=$(subscriber_to_safe_name "$sub")
|
|
501
|
+
mkdir -p "$BUS_DIR/queues/${safe_name}"
|
|
502
|
+
echo "$event_json" >> "$BUS_DIR/queues/${safe_name}/pending.jsonl"
|
|
503
|
+
fi
|
|
504
|
+
done
|
|
505
|
+
|
|
506
|
+
log_ok "Message sent: seq=$seq -> $target"
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
# ============================================================================
|
|
510
|
+
# Command: broadcast (broadcast message)
|
|
511
|
+
# ============================================================================
|
|
512
|
+
|
|
513
|
+
cmd_broadcast() {
|
|
514
|
+
local message="${1:-}"
|
|
515
|
+
local publisher="${AI_BUS_PUBLISHER:-unknown}"
|
|
516
|
+
|
|
517
|
+
if [[ -z "$message" ]]; then
|
|
518
|
+
log_error "Usage: context-bus broadcast <message>"
|
|
519
|
+
exit 1
|
|
520
|
+
fi
|
|
521
|
+
|
|
522
|
+
ensure_bus
|
|
523
|
+
|
|
524
|
+
local seq
|
|
525
|
+
seq=$(get_next_seq)
|
|
526
|
+
local today_file="$BUS_DIR/events/$(get_date).jsonl"
|
|
527
|
+
|
|
528
|
+
local event_json
|
|
529
|
+
event_json=$(jq -cn \
|
|
530
|
+
--argjson seq "$seq" \
|
|
531
|
+
--arg ts "$(get_timestamp)" \
|
|
532
|
+
--arg publisher "$publisher" \
|
|
533
|
+
--arg message "$message" \
|
|
534
|
+
'{
|
|
535
|
+
seq: $seq,
|
|
536
|
+
ts: $ts,
|
|
537
|
+
type: "message",
|
|
538
|
+
event: "broadcast",
|
|
539
|
+
publisher: $publisher,
|
|
540
|
+
data: { message: $message }
|
|
541
|
+
}')
|
|
542
|
+
|
|
543
|
+
echo "$event_json" >> "$today_file"
|
|
544
|
+
|
|
545
|
+
# Fan out broadcast to all subscriber queues
|
|
546
|
+
local matching_subscribers
|
|
547
|
+
matching_subscribers=$(jq -r '.subscribers | keys[]' "$BUS_DIR/bus.json")
|
|
548
|
+
|
|
549
|
+
for sub in $matching_subscribers; do
|
|
550
|
+
local safe_name
|
|
551
|
+
safe_name=$(subscriber_to_safe_name "$sub")
|
|
552
|
+
mkdir -p "$BUS_DIR/queues/${safe_name}"
|
|
553
|
+
echo "$event_json" >> "$BUS_DIR/queues/${safe_name}/pending.jsonl"
|
|
554
|
+
done
|
|
555
|
+
|
|
556
|
+
log_ok "Broadcast sent: seq=$seq"
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
# ============================================================================
|
|
560
|
+
# Command: status
|
|
561
|
+
# ============================================================================
|
|
562
|
+
|
|
563
|
+
cmd_status() {
|
|
564
|
+
ensure_bus
|
|
565
|
+
|
|
566
|
+
echo ""
|
|
567
|
+
echo -e "${CYAN}=== Event Bus Status ===${NC}"
|
|
568
|
+
echo ""
|
|
569
|
+
|
|
570
|
+
local bus_id
|
|
571
|
+
bus_id=$(jq -r '.bus_id' "$BUS_DIR/bus.json")
|
|
572
|
+
echo "Bus ID: $bus_id"
|
|
573
|
+
echo ""
|
|
574
|
+
|
|
575
|
+
echo -e "${CYAN}Online subscribers:${NC}"
|
|
576
|
+
local online=()
|
|
577
|
+
while IFS=$'\t' read -r sub_id sub_pid sub_nick; do
|
|
578
|
+
[[ -z "$sub_id" ]] && continue
|
|
579
|
+
if is_pid_alive "$sub_pid"; then
|
|
580
|
+
if [[ -n "$sub_nick" && "$sub_nick" != "null" ]]; then
|
|
581
|
+
online+=("$sub_id ($sub_nick)")
|
|
582
|
+
else
|
|
583
|
+
online+=("$sub_id")
|
|
584
|
+
fi
|
|
585
|
+
fi
|
|
586
|
+
done < <(jq -r '.subscribers | to_entries[] | select(.value.status == "active") | "\(.key)\t\(.value.pid // 0)\t\(.value.nickname // "")"' "$BUS_DIR/bus.json")
|
|
587
|
+
if [[ ${#online[@]} -eq 0 ]]; then
|
|
588
|
+
echo " (none)"
|
|
589
|
+
else
|
|
590
|
+
printf " %s\n" "${online[@]}"
|
|
591
|
+
fi
|
|
592
|
+
echo ""
|
|
593
|
+
|
|
594
|
+
echo -e "${CYAN}Event statistics:${NC}"
|
|
595
|
+
local total=0
|
|
596
|
+
shopt -s nullglob
|
|
597
|
+
for f in "$BUS_DIR/events"/*.jsonl; do
|
|
598
|
+
if [[ -f "$f" ]]; then
|
|
599
|
+
local count
|
|
600
|
+
count=$(wc -l < "$f" | tr -d ' ')
|
|
601
|
+
total=$((total + count))
|
|
602
|
+
echo " $(basename "$f"): $count events"
|
|
603
|
+
fi
|
|
604
|
+
done
|
|
605
|
+
echo " Total: $total events"
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
# ============================================================================
|
|
609
|
+
# Command: consume
|
|
610
|
+
# ============================================================================
|
|
611
|
+
|
|
612
|
+
cmd_consume() {
|
|
613
|
+
local subscriber="${1:-}"
|
|
614
|
+
local limit="${2:-10}"
|
|
615
|
+
|
|
616
|
+
if [[ -z "$subscriber" ]]; then
|
|
617
|
+
log_error "Usage: context-bus consume <subscriber-id> [limit]"
|
|
618
|
+
exit 1
|
|
619
|
+
fi
|
|
620
|
+
|
|
621
|
+
ensure_bus
|
|
622
|
+
|
|
623
|
+
local safe_name
|
|
624
|
+
safe_name=$(subscriber_to_safe_name "$subscriber")
|
|
625
|
+
local offset_file="$BUS_DIR/offsets/${safe_name}.offset"
|
|
626
|
+
|
|
627
|
+
if [[ ! -f "$offset_file" ]]; then
|
|
628
|
+
log_error "Subscriber not registered: $subscriber"
|
|
629
|
+
exit 1
|
|
630
|
+
fi
|
|
631
|
+
|
|
632
|
+
local current_seq
|
|
633
|
+
current_seq=$(jq -r '.current_seq' "$offset_file")
|
|
634
|
+
|
|
635
|
+
local events=()
|
|
636
|
+
local max_seq=$current_seq
|
|
637
|
+
|
|
638
|
+
shopt -s nullglob
|
|
639
|
+
for event_file in "$BUS_DIR/events"/*.jsonl; do
|
|
640
|
+
while IFS= read -r line; do
|
|
641
|
+
local seq target
|
|
642
|
+
seq=$(echo "$line" | jq -r '.seq')
|
|
643
|
+
target=$(echo "$line" | jq -r '.target // ""')
|
|
644
|
+
|
|
645
|
+
if [[ $seq -gt $current_seq ]]; then
|
|
646
|
+
if target_matches "$target" "$subscriber"; then
|
|
647
|
+
events+=("$line")
|
|
648
|
+
[[ $seq -gt $max_seq ]] && max_seq=$seq
|
|
649
|
+
fi
|
|
650
|
+
fi
|
|
651
|
+
done < "$event_file"
|
|
652
|
+
done
|
|
653
|
+
|
|
654
|
+
local count=0
|
|
655
|
+
for event in "${events[@]}"; do
|
|
656
|
+
[[ $count -ge $limit ]] && break
|
|
657
|
+
echo "$event"
|
|
658
|
+
((count++))
|
|
659
|
+
done
|
|
660
|
+
|
|
661
|
+
if [[ $max_seq -gt $current_seq ]]; then
|
|
662
|
+
jq --argjson seq "$max_seq" --arg ts "$(get_timestamp)" \
|
|
663
|
+
'.current_seq = $seq | .last_consumed_at = $ts' \
|
|
664
|
+
"$offset_file" > "${offset_file}.tmp"
|
|
665
|
+
mv "${offset_file}.tmp" "$offset_file"
|
|
666
|
+
fi
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
# ============================================================================
|
|
670
|
+
# Command: resolve (smart routing - find target agent)
|
|
671
|
+
# ============================================================================
|
|
672
|
+
|
|
673
|
+
cmd_resolve() {
|
|
674
|
+
local my_id="${1:-}"
|
|
675
|
+
local target_type="${2:-}"
|
|
676
|
+
|
|
677
|
+
if [[ -z "$my_id" || -z "$target_type" ]]; then
|
|
678
|
+
log_error "Usage: bus resolve <my-subscriber-id> <target-type>"
|
|
679
|
+
log_error "Example: bus resolve claude-code:abc123 codex"
|
|
680
|
+
exit 1
|
|
681
|
+
fi
|
|
682
|
+
|
|
683
|
+
ensure_bus
|
|
684
|
+
|
|
685
|
+
echo ""
|
|
686
|
+
echo -e "${CYAN}=== Smart Routing: Finding $target_type ===${NC}"
|
|
687
|
+
echo ""
|
|
688
|
+
|
|
689
|
+
# Get all active subscribers of target type (excluding myself) that are currently online.
|
|
690
|
+
local candidates=()
|
|
691
|
+
while IFS=$'\t' read -r candidate_id candidate_pid; do
|
|
692
|
+
[[ -z "$candidate_id" ]] && continue
|
|
693
|
+
if is_pid_alive "$candidate_pid"; then
|
|
694
|
+
candidates+=("$candidate_id")
|
|
695
|
+
fi
|
|
696
|
+
done < <(jq -r --arg type "$target_type" --arg me "$my_id" '
|
|
697
|
+
.subscribers | to_entries[] |
|
|
698
|
+
select(.value.agent_type == $type and .key != $me and .value.status == "active") |
|
|
699
|
+
"\(.key)\t\(.value.pid // 0)"
|
|
700
|
+
' "$BUS_DIR/bus.json")
|
|
701
|
+
|
|
702
|
+
if [[ ${#candidates[@]} -eq 0 ]]; then
|
|
703
|
+
log_warn "No online $target_type agents found"
|
|
704
|
+
echo ""
|
|
705
|
+
echo "RESULT: none"
|
|
706
|
+
return 0
|
|
707
|
+
fi
|
|
708
|
+
|
|
709
|
+
# Count candidates
|
|
710
|
+
local count
|
|
711
|
+
count=${#candidates[@]}
|
|
712
|
+
|
|
713
|
+
if [[ "$count" -eq 1 ]]; then
|
|
714
|
+
echo -e "${GREEN}Only one $target_type found:${NC} ${candidates[0]}"
|
|
715
|
+
echo ""
|
|
716
|
+
echo "RESULT: ${candidates[0]}"
|
|
717
|
+
return 0
|
|
718
|
+
fi
|
|
719
|
+
|
|
720
|
+
# Multiple candidates - show each with message history
|
|
721
|
+
echo -e "${YELLOW}Multiple $target_type agents found ($count):${NC}"
|
|
722
|
+
echo ""
|
|
723
|
+
|
|
724
|
+
for candidate in "${candidates[@]}"; do
|
|
725
|
+
local nickname
|
|
726
|
+
nickname=$(jq -r --arg id "$candidate" '.subscribers[$id].nickname // ""' "$BUS_DIR/bus.json")
|
|
727
|
+
local joined_at
|
|
728
|
+
joined_at=$(jq -r --arg id "$candidate" '.subscribers[$id].joined_at // ""' "$BUS_DIR/bus.json")
|
|
729
|
+
|
|
730
|
+
echo -e "${CYAN}[$candidate]${NC}"
|
|
731
|
+
if [[ -n "$nickname" && "$nickname" != "null" ]]; then
|
|
732
|
+
echo " Nickname: $nickname"
|
|
733
|
+
fi
|
|
734
|
+
echo " Joined: $joined_at"
|
|
735
|
+
|
|
736
|
+
# Find recent message history with this candidate
|
|
737
|
+
echo " Recent messages:"
|
|
738
|
+
local msg_count=0
|
|
739
|
+
shopt -s nullglob
|
|
740
|
+
for event_file in "$BUS_DIR/events"/*.jsonl; do
|
|
741
|
+
while IFS= read -r line; do
|
|
742
|
+
local publisher target msg_preview
|
|
743
|
+
publisher=$(echo "$line" | jq -r '.publisher // ""')
|
|
744
|
+
target=$(echo "$line" | jq -r '.target // ""')
|
|
745
|
+
|
|
746
|
+
# Check if this message involves both my_id and candidate
|
|
747
|
+
if [[ ("$publisher" == "$my_id" && "$target" == "$candidate") || \
|
|
748
|
+
("$publisher" == "$candidate" && "$target" == "$my_id") ]]; then
|
|
749
|
+
msg_preview=$(echo "$line" | jq -r '.data.message // "" | .[0:80]')
|
|
750
|
+
local direction
|
|
751
|
+
if [[ "$publisher" == "$my_id" ]]; then
|
|
752
|
+
direction="→ sent"
|
|
753
|
+
else
|
|
754
|
+
direction="← recv"
|
|
755
|
+
fi
|
|
756
|
+
echo " $direction: $msg_preview..."
|
|
757
|
+
((msg_count++))
|
|
758
|
+
if [[ $msg_count -ge 3 ]]; then
|
|
759
|
+
break 2
|
|
760
|
+
fi
|
|
761
|
+
fi
|
|
762
|
+
done < "$event_file"
|
|
763
|
+
done
|
|
764
|
+
|
|
765
|
+
if [[ $msg_count -eq 0 ]]; then
|
|
766
|
+
echo " (no message history)"
|
|
767
|
+
fi
|
|
768
|
+
echo ""
|
|
769
|
+
done
|
|
770
|
+
|
|
771
|
+
echo "---"
|
|
772
|
+
echo "CANDIDATES: ${candidates[*]}"
|
|
773
|
+
echo ""
|
|
774
|
+
echo ""
|
|
775
|
+
echo -e "${CYAN}Hint: Use message history and context to choose the right target.${NC}"
|
|
776
|
+
echo "If unsure, you can broadcast to all: ufoo bus send \"$target_type\" \"message\""
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
# ============================================================================
|
|
780
|
+
# Command: rename (set/change nickname)
|
|
781
|
+
# ============================================================================
|
|
782
|
+
|
|
783
|
+
cmd_rename() {
|
|
784
|
+
local subscriber="${1:-}"
|
|
785
|
+
local new_nickname="${2:-}"
|
|
786
|
+
|
|
787
|
+
if [[ -z "$subscriber" || -z "$new_nickname" ]]; then
|
|
788
|
+
log_error "Usage: bus rename <subscriber-id> <new-nickname>"
|
|
789
|
+
log_error "Example: bus rename claude-code:abc123 'architect'"
|
|
790
|
+
exit 1
|
|
791
|
+
fi
|
|
792
|
+
|
|
793
|
+
ensure_bus
|
|
794
|
+
|
|
795
|
+
# Check subscriber exists
|
|
796
|
+
local exists
|
|
797
|
+
exists=$(jq -r --arg id "$subscriber" '.subscribers[$id] // empty' "$BUS_DIR/bus.json")
|
|
798
|
+
if [[ -z "$exists" ]]; then
|
|
799
|
+
log_error "Subscriber not found: $subscriber"
|
|
800
|
+
exit 1
|
|
801
|
+
fi
|
|
802
|
+
|
|
803
|
+
# Check nickname uniqueness
|
|
804
|
+
local existing
|
|
805
|
+
existing=$(resolve_nickname "$new_nickname" 2>/dev/null || echo "")
|
|
806
|
+
if [[ -n "$existing" && "$existing" != "$subscriber" ]]; then
|
|
807
|
+
log_error "Nickname '$new_nickname' already in use by $existing"
|
|
808
|
+
exit 1
|
|
809
|
+
fi
|
|
810
|
+
|
|
811
|
+
# Get old nickname
|
|
812
|
+
local old_nickname
|
|
813
|
+
old_nickname=$(jq -r --arg id "$subscriber" '.subscribers[$id].nickname // ""' "$BUS_DIR/bus.json")
|
|
814
|
+
|
|
815
|
+
# Update nickname
|
|
816
|
+
local tmp_file
|
|
817
|
+
tmp_file=$(mktemp)
|
|
818
|
+
|
|
819
|
+
jq --arg id "$subscriber" \
|
|
820
|
+
--arg nick "$new_nickname" \
|
|
821
|
+
'.subscribers[$id].nickname = $nick' \
|
|
822
|
+
"$BUS_DIR/bus.json" > "$tmp_file"
|
|
823
|
+
|
|
824
|
+
mv "$tmp_file" "$BUS_DIR/bus.json"
|
|
825
|
+
|
|
826
|
+
# Publish rename event
|
|
827
|
+
local seq
|
|
828
|
+
seq=$(get_next_seq)
|
|
829
|
+
local today_file="$BUS_DIR/events/$(get_date).jsonl"
|
|
830
|
+
|
|
831
|
+
local event_json
|
|
832
|
+
event_json=$(jq -cn \
|
|
833
|
+
--argjson seq "$seq" \
|
|
834
|
+
--arg ts "$(get_timestamp)" \
|
|
835
|
+
--arg subscriber "$subscriber" \
|
|
836
|
+
--arg old_nick "$old_nickname" \
|
|
837
|
+
--arg new_nick "$new_nickname" \
|
|
838
|
+
'{
|
|
839
|
+
seq: $seq,
|
|
840
|
+
ts: $ts,
|
|
841
|
+
type: "system",
|
|
842
|
+
event: "agent_renamed",
|
|
843
|
+
publisher: $subscriber,
|
|
844
|
+
data: {
|
|
845
|
+
subscriber: $subscriber,
|
|
846
|
+
old_nickname: $old_nick,
|
|
847
|
+
new_nickname: $new_nick
|
|
848
|
+
}
|
|
849
|
+
}')
|
|
850
|
+
|
|
851
|
+
echo "$event_json" >> "$today_file"
|
|
852
|
+
|
|
853
|
+
if [[ -n "$old_nickname" ]]; then
|
|
854
|
+
log_ok "Renamed $subscriber: '$old_nickname' -> '$new_nickname'"
|
|
855
|
+
else
|
|
856
|
+
log_ok "Set nickname for $subscriber: '$new_nickname'"
|
|
857
|
+
fi
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
# ============================================================================
|
|
861
|
+
# Command: leave
|
|
862
|
+
# ============================================================================
|
|
863
|
+
|
|
864
|
+
cmd_leave() {
|
|
865
|
+
local subscriber="${1:-}"
|
|
866
|
+
|
|
867
|
+
if [[ -z "$subscriber" ]]; then
|
|
868
|
+
log_error "Usage: context-bus leave <subscriber-id>"
|
|
869
|
+
exit 1
|
|
870
|
+
fi
|
|
871
|
+
|
|
872
|
+
ensure_bus
|
|
873
|
+
|
|
874
|
+
log_info "Leaving event bus: $subscriber"
|
|
875
|
+
|
|
876
|
+
# Update status to offline
|
|
877
|
+
local tmp_file
|
|
878
|
+
tmp_file=$(mktemp)
|
|
879
|
+
|
|
880
|
+
jq --arg name "$subscriber" \
|
|
881
|
+
'.subscribers[$name].status = "offline"' \
|
|
882
|
+
"$BUS_DIR/bus.json" > "$tmp_file"
|
|
883
|
+
|
|
884
|
+
mv "$tmp_file" "$BUS_DIR/bus.json"
|
|
885
|
+
|
|
886
|
+
log_ok "Left event bus"
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
# ============================================================================
|
|
890
|
+
# Command: alert/listen/autotrigger (helpers)
|
|
891
|
+
# ============================================================================
|
|
892
|
+
|
|
893
|
+
cmd_alert() {
|
|
894
|
+
local subscriber="${1:-}"
|
|
895
|
+
if [[ -z "$subscriber" ]]; then
|
|
896
|
+
log_error "Usage: bus alert <subscriber-id> [interval] [--notify|--daemon|--stop|...]"
|
|
897
|
+
exit 1
|
|
898
|
+
fi
|
|
899
|
+
if [[ ! -x "$SCRIPT_DIR/bus-alert.sh" ]]; then
|
|
900
|
+
log_error "Missing script: $SCRIPT_DIR/bus-alert.sh"
|
|
901
|
+
exit 1
|
|
902
|
+
fi
|
|
903
|
+
exec bash "$SCRIPT_DIR/bus-alert.sh" "$@"
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
cmd_listen() {
|
|
907
|
+
local subscriber="${1:-}"
|
|
908
|
+
if [[ -z "$subscriber" ]]; then
|
|
909
|
+
log_error "Usage: bus listen <subscriber-id> [--from-beginning|--reset|...]"
|
|
910
|
+
exit 1
|
|
911
|
+
fi
|
|
912
|
+
if [[ ! -x "$SCRIPT_DIR/bus-listen.sh" ]]; then
|
|
913
|
+
log_error "Missing script: $SCRIPT_DIR/bus-listen.sh"
|
|
914
|
+
exit 1
|
|
915
|
+
fi
|
|
916
|
+
exec bash "$SCRIPT_DIR/bus-listen.sh" "$@"
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
cmd_autotrigger() {
|
|
920
|
+
if [[ ! -x "$SCRIPT_DIR/bus-autotrigger.sh" ]]; then
|
|
921
|
+
log_error "Missing script: $SCRIPT_DIR/bus-autotrigger.sh"
|
|
922
|
+
exit 1
|
|
923
|
+
fi
|
|
924
|
+
exec bash "$SCRIPT_DIR/bus-autotrigger.sh" "$@"
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
# ============================================================================
|
|
928
|
+
# Main entry
|
|
929
|
+
# ============================================================================
|
|
930
|
+
|
|
931
|
+
main() {
|
|
932
|
+
local cmd="${1:-help}"
|
|
933
|
+
shift || true
|
|
934
|
+
|
|
935
|
+
case "$cmd" in
|
|
936
|
+
init) cmd_init "$@" ;;
|
|
937
|
+
join) cmd_join "$@" ;;
|
|
938
|
+
check) cmd_check "$@" ;;
|
|
939
|
+
ack) cmd_ack "$@" ;;
|
|
940
|
+
send) cmd_send "$@" ;;
|
|
941
|
+
broadcast) cmd_broadcast "$@" ;;
|
|
942
|
+
status) cmd_status "$@" ;;
|
|
943
|
+
consume) cmd_consume "$@" ;;
|
|
944
|
+
resolve) cmd_resolve "$@" ;;
|
|
945
|
+
rename|nick) cmd_rename "$@" ;;
|
|
946
|
+
leave) cmd_leave "$@" ;;
|
|
947
|
+
alert) cmd_alert "$@" ;;
|
|
948
|
+
listen) cmd_listen "$@" ;;
|
|
949
|
+
autotrigger) cmd_autotrigger "$@" ;;
|
|
950
|
+
help|--help|-h)
|
|
951
|
+
echo "bus - Project-level Agent event bus"
|
|
952
|
+
echo ""
|
|
953
|
+
echo "Usage: bus <command> [options]"
|
|
954
|
+
echo ""
|
|
955
|
+
echo "Commands:"
|
|
956
|
+
echo " init Initialize event bus"
|
|
957
|
+
echo " join [session-id] [type] [nick] Join bus (auto-generates nickname if omitted)"
|
|
958
|
+
echo " check <subscriber> Check pending events"
|
|
959
|
+
echo " ack <subscriber> Acknowledge and clear pending messages"
|
|
960
|
+
echo " resolve <my-id> <target-type> Smart routing: find target agent"
|
|
961
|
+
echo " rename <subscriber> <nickname> Set/change agent nickname"
|
|
962
|
+
echo " send <target> <message> Send targeted message (supports nickname)"
|
|
963
|
+
echo " broadcast <message> Broadcast message"
|
|
964
|
+
echo " status View bus status"
|
|
965
|
+
echo " consume <subscriber> Consume events"
|
|
966
|
+
echo " leave <subscriber> Leave bus"
|
|
967
|
+
echo " alert <subscriber> Background alerts (no auto-execute)"
|
|
968
|
+
echo " listen <subscriber> Foreground listener, print new messages"
|
|
969
|
+
echo " autotrigger start|stop|status Unattended auto-execute (tmux)"
|
|
970
|
+
echo ""
|
|
971
|
+
echo "Examples:"
|
|
972
|
+
echo " bus join abc123 claude-code \"architect\""
|
|
973
|
+
echo " bus rename claude-code:abc123 \"dev-lead\""
|
|
974
|
+
echo " bus send architect \"Please help me review\""
|
|
975
|
+
echo " bus send claude-code:abc123 \"Please help me review\""
|
|
976
|
+
;;
|
|
977
|
+
*)
|
|
978
|
+
log_error "Unknown command: $cmd"
|
|
979
|
+
exit 1
|
|
980
|
+
;;
|
|
981
|
+
esac
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
main "$@"
|