u-foo 1.0.6 → 1.1.9

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.
Files changed (149) hide show
  1. package/README.md +44 -4
  2. package/SKILLS/ufoo/SKILL.md +17 -2
  3. package/SKILLS/uinit/SKILL.md +8 -3
  4. package/bin/ucode-core.js +15 -0
  5. package/bin/ucode.js +125 -0
  6. package/bin/ufoo-assistant-agent.js +5 -0
  7. package/bin/ufoo-engine.js +25 -0
  8. package/bin/ufoo.js +4 -0
  9. package/modules/AGENTS.template.md +14 -4
  10. package/modules/bus/README.md +8 -5
  11. package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
  12. package/modules/context/SKILLS/uctx/SKILL.md +3 -1
  13. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  14. package/package.json +12 -3
  15. package/scripts/import-pi-mono.js +124 -0
  16. package/scripts/postinstall.js +20 -49
  17. package/scripts/sync-claude-skills.sh +21 -0
  18. package/src/agent/cliRunner.js +524 -31
  19. package/src/agent/internalRunner.js +76 -9
  20. package/src/agent/launcher.js +97 -45
  21. package/src/agent/normalizeOutput.js +1 -1
  22. package/src/agent/notifier.js +144 -4
  23. package/src/agent/ptyRunner.js +480 -10
  24. package/src/agent/ptyWrapper.js +28 -3
  25. package/src/agent/readyDetector.js +16 -0
  26. package/src/agent/ucode.js +443 -0
  27. package/src/agent/ucodeBootstrap.js +113 -0
  28. package/src/agent/ucodeBuild.js +67 -0
  29. package/src/agent/ucodeDoctor.js +184 -0
  30. package/src/agent/ucodeRuntimeConfig.js +129 -0
  31. package/src/agent/ufooAgent.js +11 -2
  32. package/src/assistant/agent.js +260 -0
  33. package/src/assistant/bridge.js +172 -0
  34. package/src/assistant/engine.js +252 -0
  35. package/src/assistant/stdio.js +58 -0
  36. package/src/assistant/ufooEngineCli.js +306 -0
  37. package/src/bus/activate.js +27 -11
  38. package/src/bus/daemon.js +133 -5
  39. package/src/bus/index.js +137 -80
  40. package/src/bus/inject.js +47 -17
  41. package/src/bus/message.js +145 -17
  42. package/src/bus/nickname.js +3 -1
  43. package/src/bus/queue.js +6 -1
  44. package/src/bus/store.js +189 -0
  45. package/src/bus/subscriber.js +20 -4
  46. package/src/bus/utils.js +9 -3
  47. package/src/chat/agentBar.js +117 -0
  48. package/src/chat/agentDirectory.js +88 -0
  49. package/src/chat/agentSockets.js +225 -0
  50. package/src/chat/agentViewController.js +298 -0
  51. package/src/chat/chatLogController.js +115 -0
  52. package/src/chat/commandExecutor.js +700 -0
  53. package/src/chat/commands.js +132 -0
  54. package/src/chat/completionController.js +414 -0
  55. package/src/chat/cronScheduler.js +160 -0
  56. package/src/chat/daemonConnection.js +166 -0
  57. package/src/chat/daemonCoordinator.js +64 -0
  58. package/src/chat/daemonMessageRouter.js +257 -0
  59. package/src/chat/daemonReconnect.js +41 -0
  60. package/src/chat/daemonTransport.js +36 -0
  61. package/src/chat/daemonTransportDefaults.js +10 -0
  62. package/src/chat/dashboardKeyController.js +480 -0
  63. package/src/chat/dashboardView.js +154 -0
  64. package/src/chat/index.js +935 -2909
  65. package/src/chat/inputHistoryController.js +105 -0
  66. package/src/chat/inputListenerController.js +304 -0
  67. package/src/chat/inputMath.js +104 -0
  68. package/src/chat/inputSubmitHandler.js +171 -0
  69. package/src/chat/layout.js +165 -0
  70. package/src/chat/pasteController.js +81 -0
  71. package/src/chat/rawKeyMap.js +42 -0
  72. package/src/chat/settingsController.js +132 -0
  73. package/src/chat/statusLineController.js +177 -0
  74. package/src/chat/streamTracker.js +138 -0
  75. package/src/chat/text.js +70 -0
  76. package/src/chat/transport.js +61 -0
  77. package/src/cli/busCoreCommands.js +59 -0
  78. package/src/cli/ctxCoreCommands.js +199 -0
  79. package/src/cli/onlineCoreCommands.js +379 -0
  80. package/src/cli.js +741 -238
  81. package/src/code/README.md +29 -0
  82. package/src/code/UCODE_PROMPT.md +32 -0
  83. package/src/code/agent.js +1651 -0
  84. package/src/code/cli.js +158 -0
  85. package/src/code/config +0 -0
  86. package/src/code/dispatch.js +42 -0
  87. package/src/code/index.js +70 -0
  88. package/src/code/nativeRunner.js +1213 -0
  89. package/src/code/runtime.js +154 -0
  90. package/src/code/sessionStore.js +162 -0
  91. package/src/code/taskDecomposer.js +269 -0
  92. package/src/code/tools/bash.js +53 -0
  93. package/src/code/tools/common.js +42 -0
  94. package/src/code/tools/edit.js +70 -0
  95. package/src/code/tools/read.js +44 -0
  96. package/src/code/tools/write.js +35 -0
  97. package/src/code/tui.js +1580 -0
  98. package/src/config.js +47 -1
  99. package/src/context/decisions.js +12 -2
  100. package/src/context/index.js +18 -1
  101. package/src/context/sync.js +127 -0
  102. package/src/daemon/agentProcessManager.js +74 -0
  103. package/src/daemon/cronOps.js +241 -0
  104. package/src/daemon/index.js +661 -488
  105. package/src/daemon/ipcServer.js +99 -0
  106. package/src/daemon/ops.js +417 -179
  107. package/src/daemon/promptLoop.js +319 -0
  108. package/src/daemon/promptRequest.js +101 -0
  109. package/src/daemon/providerSessions.js +32 -17
  110. package/src/daemon/reporting.js +90 -0
  111. package/src/daemon/run.js +2 -5
  112. package/src/daemon/status.js +24 -1
  113. package/src/init/index.js +68 -14
  114. package/src/online/bridge.js +663 -0
  115. package/src/online/client.js +245 -0
  116. package/src/online/runner.js +253 -0
  117. package/src/online/server.js +992 -0
  118. package/src/online/tokens.js +103 -0
  119. package/src/report/store.js +331 -0
  120. package/src/shared/eventContract.js +35 -0
  121. package/src/shared/ptySocketContract.js +21 -0
  122. package/src/status/index.js +50 -17
  123. package/src/terminal/adapterContract.js +87 -0
  124. package/src/terminal/adapterRouter.js +84 -0
  125. package/src/terminal/adapters/externalAdapter.js +14 -0
  126. package/src/terminal/adapters/internalAdapter.js +13 -0
  127. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  128. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  129. package/src/terminal/adapters/terminalAdapter.js +31 -0
  130. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  131. package/src/ufoo/agentsStore.js +69 -3
  132. package/src/utils/banner.js +5 -2
  133. package/scripts/.archived/bash-to-js-migration/README.md +0 -46
  134. package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
  135. package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
  136. package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
  137. package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
  138. package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
  139. package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
  140. package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
  141. package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
  142. package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
  143. package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
  144. package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
  145. package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
  146. package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
  147. package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
  148. package/scripts/banner.sh +0 -2
  149. package/src/bus/API_DESIGN.md +0 -204
@@ -1,986 +0,0 @@
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
- --arg launch_mode "${UFOO_LAUNCH_MODE:-unknown}" \
299
- '
300
- .subscribers[$name] = {
301
- "agent_type": $agent_type,
302
- "instance_id": $instance_id,
303
- "nickname": $nickname,
304
- "tmux_pane": (if $tmux_pane != "" then $tmux_pane else null end),
305
- "tmux_session": (if $tmux_session != "" then $tmux_session else null end),
306
- "tty": (if $tty != "" then $tty else null end),
307
- "pid": ($pid | tonumber),
308
- "joined_at": $ts,
309
- "status": "active",
310
- "last_heartbeat": $ts,
311
- "launch_mode": $launch_mode
312
- }
313
- |
314
- .agent_types[$agent_type].instances = (
315
- (.agent_types[$agent_type].instances // []) + [$instance_id] | unique
316
- )
317
- |
318
- .agent_types[$agent_type].active_count = (
319
- .subscribers | to_entries | map(select(.value.agent_type == $agent_type and .value.status == "active")) | length
320
- )
321
- ' "$BUS_DIR/bus.json" > "$tmp_file"
322
-
323
- mv "$tmp_file" "$BUS_DIR/bus.json"
324
-
325
- # Create offset file
326
- cat > "$BUS_DIR/offsets/${safe_name}.offset" << EOF
327
- {
328
- "subscriber": "$subscriber",
329
- "current_seq": 0,
330
- "last_consumed_at": "$(get_timestamp)"
331
- }
332
- EOF
333
-
334
- # Create queue directory
335
- mkdir -p "$BUS_DIR/queues/${safe_name}"
336
- # Best-effort: persist tty for other scripts (e.g. injection) without parsing bus.json
337
- if [[ -n "$tty_path" ]]; then
338
- echo "$tty_path" > "$BUS_DIR/queues/${safe_name}/tty"
339
- fi
340
-
341
- # Record tty device (for later inject targeting)
342
- local current_tty
343
- current_tty=$(tty 2>/dev/null || echo "")
344
- if [[ -n "$current_tty" && "$current_tty" != "not a tty" ]]; then
345
- echo "$current_tty" > "$BUS_DIR/queues/${safe_name}/tty"
346
- fi
347
-
348
- # Publish join event
349
- local seq
350
- seq=$(get_next_seq)
351
- local today_file="$BUS_DIR/events/$(get_date).jsonl"
352
- echo "{\"seq\":$seq,\"ts\":\"$(get_timestamp)\",\"type\":\"system\",\"event\":\"agent_joined\",\"publisher\":\"$subscriber\",\"data\":{\"agent_type\":\"$agent_type\",\"instance_id\":\"$session_id\"}}" >> "$today_file"
353
-
354
- log_ok "Joined event bus"
355
- echo ""
356
- echo -e "${CYAN}My identity: $subscriber${NC}"
357
- echo ""
358
-
359
- # Check pending events
360
- cmd_check "$subscriber"
361
-
362
- # Output subscriber ID
363
- echo "$subscriber"
364
- }
365
-
366
- # ============================================================================
367
- # Command: check (check pending events)
368
- # ============================================================================
369
-
370
- cmd_check() {
371
- local subscriber="${1:-}"
372
- local auto_ack="${2:-}"
373
-
374
- if [[ -z "$subscriber" ]]; then
375
- log_error "Usage: bus check <subscriber-id> [--ack]"
376
- exit 1
377
- fi
378
-
379
- ensure_bus
380
-
381
- local safe_name
382
- safe_name=$(subscriber_to_safe_name "$subscriber")
383
- local queue_file="$BUS_DIR/queues/${safe_name}/pending.jsonl"
384
-
385
- if [[ -f "$queue_file" && -s "$queue_file" ]]; then
386
- local count
387
- count=$(wc -l < "$queue_file" | tr -d ' ')
388
- log_warn "You have $count pending event(s):"
389
- echo ""
390
- while IFS= read -r event; do
391
- local publisher type event_name data
392
- publisher=$(echo "$event" | jq -r '.publisher')
393
- type=$(echo "$event" | jq -r '.type')
394
- event_name=$(echo "$event" | jq -r '.event')
395
- data=$(echo "$event" | jq -c '.data')
396
- echo -e " ${YELLOW}@you${NC} from ${CYAN}$publisher${NC}"
397
- echo -e " Type: $type/$event_name"
398
- echo -e " Content: $data"
399
- echo ""
400
- done < "$queue_file"
401
-
402
- # Auto-ack if requested, or show hint
403
- if [[ "$auto_ack" == "--ack" ]]; then
404
- : > "$queue_file"
405
- log_ok "Messages acknowledged and cleared"
406
- else
407
- echo -e "${CYAN}After handling, run: ufoo bus ack $subscriber${NC}"
408
- fi
409
- else
410
- log_ok "No pending events"
411
- fi
412
- }
413
-
414
- # ============================================================================
415
- # Command: ack (acknowledge/clear pending messages)
416
- # ============================================================================
417
-
418
- cmd_ack() {
419
- local subscriber="${1:-}"
420
-
421
- if [[ -z "$subscriber" ]]; then
422
- log_error "Usage: bus ack <subscriber-id>"
423
- exit 1
424
- fi
425
-
426
- ensure_bus
427
-
428
- local safe_name
429
- safe_name=$(subscriber_to_safe_name "$subscriber")
430
- local queue_file="$BUS_DIR/queues/${safe_name}/pending.jsonl"
431
-
432
- if [[ -f "$queue_file" && -s "$queue_file" ]]; then
433
- local count
434
- count=$(wc -l < "$queue_file" | tr -d ' ')
435
- # Clear the queue
436
- : > "$queue_file"
437
- log_ok "Acknowledged and cleared $count message(s)"
438
- else
439
- log_ok "No pending messages to acknowledge"
440
- fi
441
- }
442
-
443
- # ============================================================================
444
- # Command: send (send targeted message)
445
- # ============================================================================
446
-
447
- cmd_send() {
448
- local target="${1:-}"
449
- local message="${2:-}"
450
-
451
- # Auto-detect publisher: prefer env var, otherwise build from session ID
452
- local publisher="${AI_BUS_PUBLISHER:-}"
453
- if [[ -z "$publisher" ]]; then
454
- if [[ -n "${CODEX_SESSION_ID:-}" ]]; then
455
- publisher="codex:${CODEX_SESSION_ID}"
456
- elif [[ -n "${CLAUDE_SESSION_ID:-}" ]]; then
457
- publisher="claude-code:${CLAUDE_SESSION_ID}"
458
- else
459
- publisher="unknown"
460
- fi
461
- fi
462
-
463
- if [[ -z "$target" || -z "$message" ]]; then
464
- log_error "Usage: context-bus send <target> <message>"
465
- log_error "Example: context-bus send claude-code:abc123 'Please help me review'"
466
- exit 1
467
- fi
468
-
469
- ensure_bus
470
-
471
- local seq
472
- seq=$(get_next_seq)
473
- local today_file="$BUS_DIR/events/$(get_date).jsonl"
474
-
475
- # Build event
476
- local event_json
477
- event_json=$(jq -cn \
478
- --argjson seq "$seq" \
479
- --arg ts "$(get_timestamp)" \
480
- --arg publisher "$publisher" \
481
- --arg target "$target" \
482
- --arg message "$message" \
483
- '{
484
- seq: $seq,
485
- ts: $ts,
486
- type: "message",
487
- event: "targeted",
488
- publisher: $publisher,
489
- target: $target,
490
- data: { message: $message }
491
- }')
492
-
493
- echo "$event_json" >> "$today_file"
494
-
495
- # Write to target queue
496
- local matching_subscribers
497
- matching_subscribers=$(jq -r '.subscribers | keys[]' "$BUS_DIR/bus.json")
498
-
499
- for sub in $matching_subscribers; do
500
- if target_matches "$target" "$sub"; then
501
- local safe_name
502
- safe_name=$(subscriber_to_safe_name "$sub")
503
- mkdir -p "$BUS_DIR/queues/${safe_name}"
504
- echo "$event_json" >> "$BUS_DIR/queues/${safe_name}/pending.jsonl"
505
- fi
506
- done
507
-
508
- log_ok "Message sent: seq=$seq -> $target"
509
- }
510
-
511
- # ============================================================================
512
- # Command: broadcast (broadcast message)
513
- # ============================================================================
514
-
515
- cmd_broadcast() {
516
- local message="${1:-}"
517
- local publisher="${AI_BUS_PUBLISHER:-unknown}"
518
-
519
- if [[ -z "$message" ]]; then
520
- log_error "Usage: context-bus broadcast <message>"
521
- exit 1
522
- fi
523
-
524
- ensure_bus
525
-
526
- local seq
527
- seq=$(get_next_seq)
528
- local today_file="$BUS_DIR/events/$(get_date).jsonl"
529
-
530
- local event_json
531
- event_json=$(jq -cn \
532
- --argjson seq "$seq" \
533
- --arg ts "$(get_timestamp)" \
534
- --arg publisher "$publisher" \
535
- --arg message "$message" \
536
- '{
537
- seq: $seq,
538
- ts: $ts,
539
- type: "message",
540
- event: "broadcast",
541
- publisher: $publisher,
542
- data: { message: $message }
543
- }')
544
-
545
- echo "$event_json" >> "$today_file"
546
-
547
- # Fan out broadcast to all subscriber queues
548
- local matching_subscribers
549
- matching_subscribers=$(jq -r '.subscribers | keys[]' "$BUS_DIR/bus.json")
550
-
551
- for sub in $matching_subscribers; do
552
- local safe_name
553
- safe_name=$(subscriber_to_safe_name "$sub")
554
- mkdir -p "$BUS_DIR/queues/${safe_name}"
555
- echo "$event_json" >> "$BUS_DIR/queues/${safe_name}/pending.jsonl"
556
- done
557
-
558
- log_ok "Broadcast sent: seq=$seq"
559
- }
560
-
561
- # ============================================================================
562
- # Command: status
563
- # ============================================================================
564
-
565
- cmd_status() {
566
- ensure_bus
567
-
568
- echo ""
569
- echo -e "${CYAN}=== Event Bus Status ===${NC}"
570
- echo ""
571
-
572
- local bus_id
573
- bus_id=$(jq -r '.bus_id' "$BUS_DIR/bus.json")
574
- echo "Bus ID: $bus_id"
575
- echo ""
576
-
577
- echo -e "${CYAN}Online subscribers:${NC}"
578
- local online=()
579
- while IFS=$'\t' read -r sub_id sub_pid sub_nick; do
580
- [[ -z "$sub_id" ]] && continue
581
- if is_pid_alive "$sub_pid"; then
582
- if [[ -n "$sub_nick" && "$sub_nick" != "null" ]]; then
583
- online+=("$sub_id ($sub_nick)")
584
- else
585
- online+=("$sub_id")
586
- fi
587
- fi
588
- done < <(jq -r '.subscribers | to_entries[] | select(.value.status == "active") | "\(.key)\t\(.value.pid // 0)\t\(.value.nickname // "")"' "$BUS_DIR/bus.json")
589
- if [[ ${#online[@]} -eq 0 ]]; then
590
- echo " (none)"
591
- else
592
- printf " %s\n" "${online[@]}"
593
- fi
594
- echo ""
595
-
596
- echo -e "${CYAN}Event statistics:${NC}"
597
- local total=0
598
- shopt -s nullglob
599
- for f in "$BUS_DIR/events"/*.jsonl; do
600
- if [[ -f "$f" ]]; then
601
- local count
602
- count=$(wc -l < "$f" | tr -d ' ')
603
- total=$((total + count))
604
- echo " $(basename "$f"): $count events"
605
- fi
606
- done
607
- echo " Total: $total events"
608
- }
609
-
610
- # ============================================================================
611
- # Command: consume
612
- # ============================================================================
613
-
614
- cmd_consume() {
615
- local subscriber="${1:-}"
616
- local limit="${2:-10}"
617
-
618
- if [[ -z "$subscriber" ]]; then
619
- log_error "Usage: context-bus consume <subscriber-id> [limit]"
620
- exit 1
621
- fi
622
-
623
- ensure_bus
624
-
625
- local safe_name
626
- safe_name=$(subscriber_to_safe_name "$subscriber")
627
- local offset_file="$BUS_DIR/offsets/${safe_name}.offset"
628
-
629
- if [[ ! -f "$offset_file" ]]; then
630
- log_error "Subscriber not registered: $subscriber"
631
- exit 1
632
- fi
633
-
634
- local current_seq
635
- current_seq=$(jq -r '.current_seq' "$offset_file")
636
-
637
- local events=()
638
- local max_seq=$current_seq
639
-
640
- shopt -s nullglob
641
- for event_file in "$BUS_DIR/events"/*.jsonl; do
642
- while IFS= read -r line; do
643
- local seq target
644
- seq=$(echo "$line" | jq -r '.seq')
645
- target=$(echo "$line" | jq -r '.target // ""')
646
-
647
- if [[ $seq -gt $current_seq ]]; then
648
- if target_matches "$target" "$subscriber"; then
649
- events+=("$line")
650
- [[ $seq -gt $max_seq ]] && max_seq=$seq
651
- fi
652
- fi
653
- done < "$event_file"
654
- done
655
-
656
- local count=0
657
- for event in "${events[@]}"; do
658
- [[ $count -ge $limit ]] && break
659
- echo "$event"
660
- ((count++))
661
- done
662
-
663
- if [[ $max_seq -gt $current_seq ]]; then
664
- jq --argjson seq "$max_seq" --arg ts "$(get_timestamp)" \
665
- '.current_seq = $seq | .last_consumed_at = $ts' \
666
- "$offset_file" > "${offset_file}.tmp"
667
- mv "${offset_file}.tmp" "$offset_file"
668
- fi
669
- }
670
-
671
- # ============================================================================
672
- # Command: resolve (smart routing - find target agent)
673
- # ============================================================================
674
-
675
- cmd_resolve() {
676
- local my_id="${1:-}"
677
- local target_type="${2:-}"
678
-
679
- if [[ -z "$my_id" || -z "$target_type" ]]; then
680
- log_error "Usage: bus resolve <my-subscriber-id> <target-type>"
681
- log_error "Example: bus resolve claude-code:abc123 codex"
682
- exit 1
683
- fi
684
-
685
- ensure_bus
686
-
687
- echo ""
688
- echo -e "${CYAN}=== Smart Routing: Finding $target_type ===${NC}"
689
- echo ""
690
-
691
- # Get all active subscribers of target type (excluding myself) that are currently online.
692
- local candidates=()
693
- while IFS=$'\t' read -r candidate_id candidate_pid; do
694
- [[ -z "$candidate_id" ]] && continue
695
- if is_pid_alive "$candidate_pid"; then
696
- candidates+=("$candidate_id")
697
- fi
698
- done < <(jq -r --arg type "$target_type" --arg me "$my_id" '
699
- .subscribers | to_entries[] |
700
- select(.value.agent_type == $type and .key != $me and .value.status == "active") |
701
- "\(.key)\t\(.value.pid // 0)"
702
- ' "$BUS_DIR/bus.json")
703
-
704
- if [[ ${#candidates[@]} -eq 0 ]]; then
705
- log_warn "No online $target_type agents found"
706
- echo ""
707
- echo "RESULT: none"
708
- return 0
709
- fi
710
-
711
- # Count candidates
712
- local count
713
- count=${#candidates[@]}
714
-
715
- if [[ "$count" -eq 1 ]]; then
716
- echo -e "${GREEN}Only one $target_type found:${NC} ${candidates[0]}"
717
- echo ""
718
- echo "RESULT: ${candidates[0]}"
719
- return 0
720
- fi
721
-
722
- # Multiple candidates - show each with message history
723
- echo -e "${YELLOW}Multiple $target_type agents found ($count):${NC}"
724
- echo ""
725
-
726
- for candidate in "${candidates[@]}"; do
727
- local nickname
728
- nickname=$(jq -r --arg id "$candidate" '.subscribers[$id].nickname // ""' "$BUS_DIR/bus.json")
729
- local joined_at
730
- joined_at=$(jq -r --arg id "$candidate" '.subscribers[$id].joined_at // ""' "$BUS_DIR/bus.json")
731
-
732
- echo -e "${CYAN}[$candidate]${NC}"
733
- if [[ -n "$nickname" && "$nickname" != "null" ]]; then
734
- echo " Nickname: $nickname"
735
- fi
736
- echo " Joined: $joined_at"
737
-
738
- # Find recent message history with this candidate
739
- echo " Recent messages:"
740
- local msg_count=0
741
- shopt -s nullglob
742
- for event_file in "$BUS_DIR/events"/*.jsonl; do
743
- while IFS= read -r line; do
744
- local publisher target msg_preview
745
- publisher=$(echo "$line" | jq -r '.publisher // ""')
746
- target=$(echo "$line" | jq -r '.target // ""')
747
-
748
- # Check if this message involves both my_id and candidate
749
- if [[ ("$publisher" == "$my_id" && "$target" == "$candidate") || \
750
- ("$publisher" == "$candidate" && "$target" == "$my_id") ]]; then
751
- msg_preview=$(echo "$line" | jq -r '.data.message // "" | .[0:80]')
752
- local direction
753
- if [[ "$publisher" == "$my_id" ]]; then
754
- direction="→ sent"
755
- else
756
- direction="← recv"
757
- fi
758
- echo " $direction: $msg_preview..."
759
- ((msg_count++))
760
- if [[ $msg_count -ge 3 ]]; then
761
- break 2
762
- fi
763
- fi
764
- done < "$event_file"
765
- done
766
-
767
- if [[ $msg_count -eq 0 ]]; then
768
- echo " (no message history)"
769
- fi
770
- echo ""
771
- done
772
-
773
- echo "---"
774
- echo "CANDIDATES: ${candidates[*]}"
775
- echo ""
776
- echo ""
777
- echo -e "${CYAN}Hint: Use message history and context to choose the right target.${NC}"
778
- echo "If unsure, you can broadcast to all: ufoo bus send \"$target_type\" \"message\""
779
- }
780
-
781
- # ============================================================================
782
- # Command: rename (set/change nickname)
783
- # ============================================================================
784
-
785
- cmd_rename() {
786
- local subscriber="${1:-}"
787
- local new_nickname="${2:-}"
788
-
789
- if [[ -z "$subscriber" || -z "$new_nickname" ]]; then
790
- log_error "Usage: bus rename <subscriber-id> <new-nickname>"
791
- log_error "Example: bus rename claude-code:abc123 'architect'"
792
- exit 1
793
- fi
794
-
795
- ensure_bus
796
-
797
- # Check subscriber exists
798
- local exists
799
- exists=$(jq -r --arg id "$subscriber" '.subscribers[$id] // empty' "$BUS_DIR/bus.json")
800
- if [[ -z "$exists" ]]; then
801
- log_error "Subscriber not found: $subscriber"
802
- exit 1
803
- fi
804
-
805
- # Check nickname uniqueness
806
- local existing
807
- existing=$(resolve_nickname "$new_nickname" 2>/dev/null || echo "")
808
- if [[ -n "$existing" && "$existing" != "$subscriber" ]]; then
809
- log_error "Nickname '$new_nickname' already in use by $existing"
810
- exit 1
811
- fi
812
-
813
- # Get old nickname
814
- local old_nickname
815
- old_nickname=$(jq -r --arg id "$subscriber" '.subscribers[$id].nickname // ""' "$BUS_DIR/bus.json")
816
-
817
- # Update nickname
818
- local tmp_file
819
- tmp_file=$(mktemp)
820
-
821
- jq --arg id "$subscriber" \
822
- --arg nick "$new_nickname" \
823
- '.subscribers[$id].nickname = $nick' \
824
- "$BUS_DIR/bus.json" > "$tmp_file"
825
-
826
- mv "$tmp_file" "$BUS_DIR/bus.json"
827
-
828
- # Publish rename event
829
- local seq
830
- seq=$(get_next_seq)
831
- local today_file="$BUS_DIR/events/$(get_date).jsonl"
832
-
833
- local event_json
834
- event_json=$(jq -cn \
835
- --argjson seq "$seq" \
836
- --arg ts "$(get_timestamp)" \
837
- --arg subscriber "$subscriber" \
838
- --arg old_nick "$old_nickname" \
839
- --arg new_nick "$new_nickname" \
840
- '{
841
- seq: $seq,
842
- ts: $ts,
843
- type: "system",
844
- event: "agent_renamed",
845
- publisher: $subscriber,
846
- data: {
847
- subscriber: $subscriber,
848
- old_nickname: $old_nick,
849
- new_nickname: $new_nick
850
- }
851
- }')
852
-
853
- echo "$event_json" >> "$today_file"
854
-
855
- if [[ -n "$old_nickname" ]]; then
856
- log_ok "Renamed $subscriber: '$old_nickname' -> '$new_nickname'"
857
- else
858
- log_ok "Set nickname for $subscriber: '$new_nickname'"
859
- fi
860
- }
861
-
862
- # ============================================================================
863
- # Command: leave
864
- # ============================================================================
865
-
866
- cmd_leave() {
867
- local subscriber="${1:-}"
868
-
869
- if [[ -z "$subscriber" ]]; then
870
- log_error "Usage: context-bus leave <subscriber-id>"
871
- exit 1
872
- fi
873
-
874
- ensure_bus
875
-
876
- log_info "Leaving event bus: $subscriber"
877
-
878
- # Update status to offline
879
- local tmp_file
880
- tmp_file=$(mktemp)
881
-
882
- jq --arg name "$subscriber" \
883
- '.subscribers[$name].status = "offline"' \
884
- "$BUS_DIR/bus.json" > "$tmp_file"
885
-
886
- mv "$tmp_file" "$BUS_DIR/bus.json"
887
-
888
- log_ok "Left event bus"
889
- }
890
-
891
- # ============================================================================
892
- # Command: alert/listen/autotrigger (helpers)
893
- # ============================================================================
894
-
895
- cmd_alert() {
896
- local subscriber="${1:-}"
897
- if [[ -z "$subscriber" ]]; then
898
- log_error "Usage: bus alert <subscriber-id> [interval] [--notify|--daemon|--stop|...]"
899
- exit 1
900
- fi
901
- if [[ ! -x "$SCRIPT_DIR/bus-alert.sh" ]]; then
902
- log_error "Missing script: $SCRIPT_DIR/bus-alert.sh"
903
- exit 1
904
- fi
905
- exec bash "$SCRIPT_DIR/bus-alert.sh" "$@"
906
- }
907
-
908
- cmd_listen() {
909
- local subscriber="${1:-}"
910
- if [[ -z "$subscriber" ]]; then
911
- log_error "Usage: bus listen <subscriber-id> [--from-beginning|--reset|...]"
912
- exit 1
913
- fi
914
- if [[ ! -x "$SCRIPT_DIR/bus-listen.sh" ]]; then
915
- log_error "Missing script: $SCRIPT_DIR/bus-listen.sh"
916
- exit 1
917
- fi
918
- exec bash "$SCRIPT_DIR/bus-listen.sh" "$@"
919
- }
920
-
921
- cmd_autotrigger() {
922
- if [[ ! -x "$SCRIPT_DIR/bus-autotrigger.sh" ]]; then
923
- log_error "Missing script: $SCRIPT_DIR/bus-autotrigger.sh"
924
- exit 1
925
- fi
926
- exec bash "$SCRIPT_DIR/bus-autotrigger.sh" "$@"
927
- }
928
-
929
- # ============================================================================
930
- # Main entry
931
- # ============================================================================
932
-
933
- main() {
934
- local cmd="${1:-help}"
935
- shift || true
936
-
937
- case "$cmd" in
938
- init) cmd_init "$@" ;;
939
- join) cmd_join "$@" ;;
940
- check) cmd_check "$@" ;;
941
- ack) cmd_ack "$@" ;;
942
- send) cmd_send "$@" ;;
943
- broadcast) cmd_broadcast "$@" ;;
944
- status) cmd_status "$@" ;;
945
- consume) cmd_consume "$@" ;;
946
- resolve) cmd_resolve "$@" ;;
947
- rename|nick) cmd_rename "$@" ;;
948
- leave) cmd_leave "$@" ;;
949
- alert) cmd_alert "$@" ;;
950
- listen) cmd_listen "$@" ;;
951
- autotrigger) cmd_autotrigger "$@" ;;
952
- help|--help|-h)
953
- echo "bus - Project-level Agent event bus"
954
- echo ""
955
- echo "Usage: bus <command> [options]"
956
- echo ""
957
- echo "Commands:"
958
- echo " init Initialize event bus"
959
- echo " join [session-id] [type] [nick] Join bus (auto-generates nickname if omitted)"
960
- echo " check <subscriber> Check pending events"
961
- echo " ack <subscriber> Acknowledge and clear pending messages"
962
- echo " resolve <my-id> <target-type> Smart routing: find target agent"
963
- echo " rename <subscriber> <nickname> Set/change agent nickname"
964
- echo " send <target> <message> Send targeted message (supports nickname)"
965
- echo " broadcast <message> Broadcast message"
966
- echo " status View bus status"
967
- echo " consume <subscriber> Consume events"
968
- echo " leave <subscriber> Leave bus"
969
- echo " alert <subscriber> Background alerts (no auto-execute)"
970
- echo " listen <subscriber> Foreground listener, print new messages"
971
- echo " autotrigger start|stop|status Unattended auto-execute (tmux)"
972
- echo ""
973
- echo "Examples:"
974
- echo " bus join abc123 claude-code \"architect\""
975
- echo " bus rename claude-code:abc123 \"dev-lead\""
976
- echo " bus send architect \"Please help me review\""
977
- echo " bus send claude-code:abc123 \"Please help me review\""
978
- ;;
979
- *)
980
- log_error "Unknown command: $cmd"
981
- exit 1
982
- ;;
983
- esac
984
- }
985
-
986
- main "$@"