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.
Files changed (77) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +163 -0
  3. package/README.zh-CN.md +163 -0
  4. package/bin/uclaude +65 -0
  5. package/bin/ucodex +65 -0
  6. package/bin/ufoo +93 -0
  7. package/bin/ufoo.js +35 -0
  8. package/modules/AGENTS.template.md +87 -0
  9. package/modules/bus/README.md +132 -0
  10. package/modules/bus/SKILLS/ubus/SKILL.md +209 -0
  11. package/modules/bus/scripts/bus-alert.sh +185 -0
  12. package/modules/bus/scripts/bus-listen.sh +117 -0
  13. package/modules/context/ASSUMPTIONS.md +7 -0
  14. package/modules/context/CONSTRAINTS.md +7 -0
  15. package/modules/context/CONTEXT-STRUCTURE.md +49 -0
  16. package/modules/context/DECISION-PROTOCOL.md +62 -0
  17. package/modules/context/HANDOFF.md +33 -0
  18. package/modules/context/README.md +82 -0
  19. package/modules/context/RULES.md +15 -0
  20. package/modules/context/SKILLS/README.md +14 -0
  21. package/modules/context/SKILLS/uctx/SKILL.md +91 -0
  22. package/modules/context/SYSTEM.md +18 -0
  23. package/modules/context/TEMPLATES/assumptions.md +4 -0
  24. package/modules/context/TEMPLATES/constraints.md +4 -0
  25. package/modules/context/TEMPLATES/decision.md +16 -0
  26. package/modules/context/TEMPLATES/project-context-readme.md +6 -0
  27. package/modules/context/TEMPLATES/system.md +3 -0
  28. package/modules/context/TEMPLATES/terminology.md +4 -0
  29. package/modules/context/TERMINOLOGY.md +10 -0
  30. package/modules/resources/ICONS/README.md +12 -0
  31. package/modules/resources/ICONS/libraries/README.md +17 -0
  32. package/modules/resources/ICONS/libraries/heroicons/LICENSE +22 -0
  33. package/modules/resources/ICONS/libraries/heroicons/README.md +15 -0
  34. package/modules/resources/ICONS/libraries/heroicons/arrow-right.svg +4 -0
  35. package/modules/resources/ICONS/libraries/heroicons/check.svg +4 -0
  36. package/modules/resources/ICONS/libraries/heroicons/chevron-down.svg +4 -0
  37. package/modules/resources/ICONS/libraries/heroicons/cog-6-tooth.svg +5 -0
  38. package/modules/resources/ICONS/libraries/heroicons/magnifying-glass.svg +4 -0
  39. package/modules/resources/ICONS/libraries/heroicons/x-mark.svg +4 -0
  40. package/modules/resources/ICONS/libraries/lucide/LICENSE +40 -0
  41. package/modules/resources/ICONS/libraries/lucide/README.md +15 -0
  42. package/modules/resources/ICONS/libraries/lucide/arrow-right.svg +15 -0
  43. package/modules/resources/ICONS/libraries/lucide/check.svg +14 -0
  44. package/modules/resources/ICONS/libraries/lucide/chevron-down.svg +14 -0
  45. package/modules/resources/ICONS/libraries/lucide/search.svg +15 -0
  46. package/modules/resources/ICONS/libraries/lucide/settings.svg +15 -0
  47. package/modules/resources/ICONS/libraries/lucide/x.svg +15 -0
  48. package/modules/resources/ICONS/rules.md +7 -0
  49. package/modules/resources/README.md +9 -0
  50. package/modules/resources/UI/ANTI-PATTERNS.md +6 -0
  51. package/modules/resources/UI/TONE.md +6 -0
  52. package/package.json +40 -0
  53. package/scripts/banner.sh +89 -0
  54. package/scripts/bus-alert.sh +6 -0
  55. package/scripts/bus-autotrigger.sh +6 -0
  56. package/scripts/bus-daemon.sh +231 -0
  57. package/scripts/bus-inject.sh +144 -0
  58. package/scripts/bus-listen.sh +6 -0
  59. package/scripts/bus.sh +984 -0
  60. package/scripts/context-decisions.sh +167 -0
  61. package/scripts/context-doctor.sh +72 -0
  62. package/scripts/context-lint.sh +110 -0
  63. package/scripts/doctor.sh +22 -0
  64. package/scripts/init.sh +247 -0
  65. package/scripts/skills.sh +113 -0
  66. package/scripts/status.sh +125 -0
  67. package/src/agent/cliRunner.js +190 -0
  68. package/src/agent/internalRunner.js +212 -0
  69. package/src/agent/normalizeOutput.js +41 -0
  70. package/src/agent/ufooAgent.js +222 -0
  71. package/src/chat/index.js +1603 -0
  72. package/src/cli.js +349 -0
  73. package/src/config.js +37 -0
  74. package/src/daemon/index.js +501 -0
  75. package/src/daemon/ops.js +120 -0
  76. package/src/daemon/run.js +41 -0
  77. package/src/daemon/status.js +78 -0
@@ -0,0 +1,40 @@
1
+ ISC License
2
+
3
+ Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2026 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2026.
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
+
17
+ ---
18
+
19
+ The MIT License (MIT) (for portions derived from Feather)
20
+
21
+ Copyright (c) 2013-2026 Cole Bemis
22
+
23
+ Permission is hereby granted, free of charge, to any person obtaining a copy
24
+ of this software and associated documentation files (the "Software"), to deal
25
+ in the Software without restriction, including without limitation the rights
26
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
27
+ copies of the Software, and to permit persons to whom the Software is
28
+ furnished to do so, subject to the following conditions:
29
+
30
+ The above copyright notice and this permission notice shall be included in all
31
+ copies or substantial portions of the Software.
32
+
33
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39
+ SOFTWARE.
40
+
@@ -0,0 +1,15 @@
1
+ # Lucide (Minimal Subset)
2
+
3
+ Source:
4
+ - Repository: https://github.com/lucide-icons/lucide
5
+ - Path: `icons/`
6
+ - License: ISC (see `LICENSE`)
7
+ - Fetched: 2026-01-27
8
+
9
+ Included icons:
10
+ - `arrow-right.svg`
11
+ - `chevron-down.svg`
12
+ - `search.svg`
13
+ - `settings.svg`
14
+ - `check.svg`
15
+ - `x.svg`
@@ -0,0 +1,15 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ >
12
+ <path d="M5 12h14" />
13
+ <path d="m12 5 7 7-7 7" />
14
+ </svg>
15
+
@@ -0,0 +1,14 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ >
12
+ <path d="M20 6 9 17l-5-5" />
13
+ </svg>
14
+
@@ -0,0 +1,14 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ >
12
+ <path d="m6 9 6 6 6-6" />
13
+ </svg>
14
+
@@ -0,0 +1,15 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ >
12
+ <path d="m21 21-4.34-4.34" />
13
+ <circle cx="11" cy="11" r="8" />
14
+ </svg>
15
+
@@ -0,0 +1,15 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ >
12
+ <path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" />
13
+ <circle cx="12" cy="12" r="3" />
14
+ </svg>
15
+
@@ -0,0 +1,15 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ >
12
+ <path d="M18 6 6 18" />
13
+ <path d="m6 6 12 12" />
14
+ </svg>
15
+
@@ -0,0 +1,7 @@
1
+ # Icon Rules
2
+
3
+ - ViewBox: 0 0 24 24
4
+ - Stroke-based icons preferred
5
+ - No gradients or shadows
6
+ - Avoid perfect symmetry
7
+ - Optical balance > geometric balance
@@ -0,0 +1,9 @@
1
+ # resources
2
+
3
+ Optional resources for AI-assisted coding workflows.
4
+
5
+ This repository intentionally contains non-core materials such as:
6
+ - `UI/` tone + anti-pattern references
7
+ - `ICONS/` reference icon subsets (with licenses)
8
+
9
+ It is designed to be installed and managed by `ufoo` under `~/.ufoo/modules/resources`.
@@ -0,0 +1,6 @@
1
+ # UI Anti-Patterns
2
+
3
+ - Over-rounded corners
4
+ - Decorative gradients
5
+ - Unnecessary animations
6
+ - Visual noise
@@ -0,0 +1,6 @@
1
+ # UI Tone
2
+
3
+ - Neutral
4
+ - Restrained
5
+ - Utility-focused
6
+ - Avoid friendliness or decoration
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "u-foo",
3
+ "version": "1.0.0",
4
+ "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "homepage": "https://ufoo.dev",
7
+ "author": "UFOO Team",
8
+ "keywords": [
9
+ "cli",
10
+ "ai",
11
+ "agent",
12
+ "multi-agent",
13
+ "claude",
14
+ "codex",
15
+ "orchestration",
16
+ "workspace"
17
+ ],
18
+ "bin": {
19
+ "ufoo": "bin/ufoo.js",
20
+ "uclaude": "bin/uclaude",
21
+ "ucodex": "bin/ucodex"
22
+ },
23
+ "files": [
24
+ "bin/",
25
+ "src/",
26
+ "scripts/",
27
+ "modules/",
28
+ "LICENSE",
29
+ "README.md"
30
+ ],
31
+ "type": "commonjs",
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "dependencies": {
36
+ "blessed": "^0.1.81",
37
+ "chalk": "^4.1.2",
38
+ "commander": "^13.1.0"
39
+ }
40
+ }
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env bash
2
+ # banner.sh - TUI startup banner for ufoo agents
3
+
4
+ # Colors
5
+ RST='\033[0m'
6
+ BLD='\033[1m'
7
+ DIM='\033[2m'
8
+ CYN='\033[0;36m'
9
+ GRN='\033[0;32m'
10
+ MAG='\033[0;35m'
11
+ WHT='\033[1;37m'
12
+ YLW='\033[0;33m'
13
+
14
+ show_banner() {
15
+ local agent_type="${1:-claude}"
16
+ local session_id="${2:-unknown}"
17
+ local subscriber="${3:-}"
18
+ local daemon_status="${4:-}"
19
+
20
+ local ACOL
21
+ if [[ "$agent_type" == "codex" ]]; then
22
+ ACOL="$GRN"
23
+ else
24
+ ACOL="$MAG"
25
+ fi
26
+
27
+ # Width matches Codex CLI (inner content = 52)
28
+ local INNER=52
29
+ local W=$INNER
30
+
31
+ # Helper: print line with exact padding
32
+ line() {
33
+ local prefix="$1"
34
+ local value="$2"
35
+ local color="$3"
36
+ local prefix_len=${#prefix}
37
+ local value_len=${#value}
38
+ local pad_len=$((INNER - prefix_len - value_len))
39
+ printf "${DIM}│${RST}${prefix}${color}${value}${RST}"
40
+ printf "%${pad_len}s" ""
41
+ printf "${DIM}│${RST}\n"
42
+ }
43
+
44
+ echo ""
45
+ printf "${DIM}╭"; printf '─%.0s' $(seq 1 $W); printf "╮${RST}\n"
46
+
47
+ # Title line with icon (compute padding from plain text lengths)
48
+ local icon_plain="<○>"
49
+ local title="UFOO · Multi-Agent Protocol"
50
+ local title_pad=$((INNER - 1 - ${#icon_plain} - 1 - ${#title}))
51
+ printf "${DIM}│${RST} ${WHT}${icon_plain}${RST} ${CYN}${BLD}UFOO${RST}${DIM} · Multi-Agent Protocol${RST}"
52
+ printf "%${title_pad}s" ""
53
+ printf "${DIM}│${RST}\n"
54
+
55
+ printf "${DIM}├"; printf '─%.0s' $(seq 1 $W); printf "┤${RST}\n"
56
+
57
+ # Agent
58
+ if [[ -n "$subscriber" ]]; then
59
+ local sub="$subscriber"
60
+ [[ ${#sub} -gt 36 ]] && sub="${sub:0:33}..."
61
+ line " Agent " "$sub" "${ACOL}${BLD}"
62
+ fi
63
+
64
+ # Daemon
65
+ if [[ -n "$daemon_status" ]]; then
66
+ line " Daemon " "$daemon_status" "${GRN}"
67
+ fi
68
+
69
+ # Online agents (daemon handles cleanup of dead agents)
70
+ if [[ -f ".ufoo/bus/bus.json" ]]; then
71
+ local online
72
+ online=$(jq -r '.subscribers | to_entries[] | select(.value.status == "active") | .key' .ufoo/bus/bus.json 2>/dev/null | grep -v "^${subscriber}$" 2>/dev/null | head -5 || true)
73
+ if [[ -n "$online" ]]; then
74
+ printf "${DIM}├"; printf '─%.0s' $(seq 1 $W); printf "┤${RST}\n"
75
+ line " Online " "" ""
76
+ while IFS= read -r agent; do
77
+ [[ -z "$agent" ]] && continue
78
+ local a="$agent"
79
+ [[ ${#a} -gt 44 ]] && a="${a:0:41}..."
80
+ line " · " "$a" "${YLW}"
81
+ done <<< "$online"
82
+ fi
83
+ fi
84
+
85
+ printf "${DIM}╰"; printf '─%.0s' $(seq 1 $W); printf "╯${RST}\n"
86
+ echo ""
87
+ }
88
+
89
+ export -f show_banner 2>/dev/null || true
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ exec bash "$repo_root/modules/bus/scripts/bus-alert.sh" "$@"
6
+
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ exec bash "$repo_root/modules/bus/scripts/bus-autotrigger.sh" "$@"
6
+
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # bus-daemon.sh
5
+ # Daemon that watches for new messages and injects /bus into target terminals.
6
+ #
7
+ # Usage:
8
+ # bash scripts/bus-daemon.sh [--interval <seconds>] [--daemon]
9
+ #
10
+ # This script monitors ALL subscribers' pending queues and injects /bus
11
+ # into their corresponding Terminal.app tabs when new messages arrive.
12
+ #
13
+ # Requirements:
14
+ # - macOS Accessibility permission for Terminal.app
15
+ # - Each subscriber's terminal should have title containing [bus:<instance-id>]
16
+
17
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ BUS_DIR=".ufoo/bus"
19
+ INTERVAL=2
20
+ DAEMON_MODE=0
21
+ PID_FILE="$BUS_DIR/.daemon.pid"
22
+ LOG_FILE="$BUS_DIR/logs/daemon.log"
23
+
24
+ usage() {
25
+ cat <<'USAGE'
26
+ bus-daemon.sh - Watch for new messages and inject /bus
27
+
28
+ Usage:
29
+ bash scripts/bus-daemon.sh [options]
30
+
31
+ Options:
32
+ --interval <n> Poll interval in seconds (default: 2)
33
+ --daemon Run in background
34
+ --stop Stop running daemon
35
+ --status Check daemon status
36
+ -h, --help Show this help
37
+
38
+ Notes:
39
+ - Requires macOS Accessibility permission
40
+ - Terminals must have title containing [bus:<instance-id>]
41
+ - Use `ufoo bus join` to set terminal title
42
+ USAGE
43
+ }
44
+
45
+ # Handle --help before other parsing
46
+ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
47
+ usage
48
+ exit 0
49
+ fi
50
+
51
+ while [[ $# -gt 0 ]]; do
52
+ case "$1" in
53
+ --interval)
54
+ INTERVAL="$2"
55
+ shift 2
56
+ ;;
57
+ --daemon)
58
+ DAEMON_MODE=1
59
+ shift
60
+ ;;
61
+ --stop)
62
+ if [[ -f "$PID_FILE" ]]; then
63
+ pid="$(cat "$PID_FILE" 2>/dev/null || true)"
64
+ if [[ -n "$pid" ]]; then
65
+ kill "$pid" 2>/dev/null && echo "[daemon] Stopped (pid=$pid)" || echo "[daemon] Not running"
66
+ fi
67
+ rm -f "$PID_FILE"
68
+ else
69
+ echo "[daemon] Not running"
70
+ fi
71
+ exit 0
72
+ ;;
73
+ --status)
74
+ if [[ -f "$PID_FILE" ]]; then
75
+ pid="$(cat "$PID_FILE" 2>/dev/null || true)"
76
+ if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
77
+ echo "[daemon] Running (pid=$pid)"
78
+ else
79
+ echo "[daemon] Not running (stale pid file)"
80
+ rm -f "$PID_FILE"
81
+ fi
82
+ else
83
+ echo "[daemon] Not running"
84
+ fi
85
+ exit 0
86
+ ;;
87
+ *)
88
+ echo "Unknown option: $1" >&2
89
+ usage >&2
90
+ exit 1
91
+ ;;
92
+ esac
93
+ done
94
+
95
+ if [[ ! -d "$BUS_DIR" ]]; then
96
+ echo "[daemon] Error: $BUS_DIR not found. Run ufoo init first." >&2
97
+ exit 1
98
+ fi
99
+
100
+ mkdir -p "$BUS_DIR/logs"
101
+
102
+ if [[ "$DAEMON_MODE" == "1" ]]; then
103
+ # Check if already running
104
+ if [[ -f "$PID_FILE" ]]; then
105
+ existing="$(cat "$PID_FILE" 2>/dev/null || true)"
106
+ if [[ -n "$existing" ]] && kill -0 "$existing" 2>/dev/null; then
107
+ echo "[daemon] Already running (pid=$existing)"
108
+ exit 0
109
+ fi
110
+ fi
111
+
112
+ echo "[daemon] Starting in background (log: $LOG_FILE)"
113
+ nohup bash "$0" --interval "$INTERVAL" >> "$LOG_FILE" 2>&1 &
114
+ echo $! > "$PID_FILE"
115
+ echo "[daemon] Started (pid=$!)"
116
+ exit 0
117
+ fi
118
+
119
+ # Record PID
120
+ echo $$ > "$PID_FILE"
121
+ trap 'rm -f "$PID_FILE" 2>/dev/null || true; rm -rf "$COUNTS_DIR" 2>/dev/null || true' EXIT
122
+
123
+ echo "[daemon] Started (pid=$$, interval=${INTERVAL}s)"
124
+ echo "[daemon] Watching: $BUS_DIR/queues/*/pending.jsonl"
125
+
126
+ # Use temp directory to track last known counts (bash 3.x compatible)
127
+ COUNTS_DIR="$BUS_DIR/.daemon-counts.$$"
128
+ mkdir -p "$COUNTS_DIR"
129
+
130
+ get_last_count() {
131
+ local safe_name="$1"
132
+ local count_file="$COUNTS_DIR/$safe_name"
133
+ if [[ -f "$count_file" ]]; then
134
+ cat "$count_file"
135
+ else
136
+ echo "0"
137
+ fi
138
+ }
139
+
140
+ set_last_count() {
141
+ local safe_name="$1"
142
+ local count="$2"
143
+ echo "$count" > "$COUNTS_DIR/$safe_name"
144
+ }
145
+
146
+ # Cleanup dead agents (check PID and mark offline)
147
+ cleanup_dead_agents() {
148
+ if [[ ! -f "$BUS_DIR/bus.json" ]]; then
149
+ return
150
+ fi
151
+
152
+ local changed=0
153
+ local tmp_file
154
+ tmp_file=$(mktemp)
155
+
156
+ # Get all active subscribers with their PIDs
157
+ while IFS='|' read -r subscriber pid; do
158
+ [[ -z "$subscriber" || -z "$pid" ]] && continue
159
+
160
+ # Check if PID is still running AND is a claude/codex/node process
161
+ local proc_cmd
162
+ proc_cmd=$(ps -p "$pid" -o comm= 2>/dev/null || true)
163
+ if [[ -z "$proc_cmd" ]] || ! echo "$proc_cmd" | grep -qiE "(claude|codex|node)"; then
164
+ echo "[daemon] $(date '+%H:%M:%S') Agent $subscriber (pid=$pid) is dead, marking offline"
165
+
166
+ # Mark as offline in bus.json
167
+ jq --arg name "$subscriber" \
168
+ '.subscribers[$name].status = "offline"' \
169
+ "$BUS_DIR/bus.json" > "$tmp_file" && mv "$tmp_file" "$BUS_DIR/bus.json"
170
+ tmp_file=$(mktemp)
171
+
172
+ # Clean up queue directory
173
+ local safe_name="${subscriber//:/_}"
174
+ rm -rf "$BUS_DIR/queues/${safe_name}" 2>/dev/null || true
175
+ rm -f "$BUS_DIR/offsets/${safe_name}.offset" 2>/dev/null || true
176
+
177
+ changed=1
178
+ fi
179
+ done < <(jq -r '.subscribers | to_entries[] | select(.value.status == "active") | "\(.key)|\(.value.pid)"' "$BUS_DIR/bus.json" 2>/dev/null)
180
+
181
+ rm -f "$tmp_file" 2>/dev/null || true
182
+ }
183
+
184
+ CLEANUP_COUNTER=0
185
+ CLEANUP_INTERVAL=5 # Run cleanup every 5 cycles (10 seconds with default interval)
186
+
187
+ while true; do
188
+ # Periodic cleanup of dead agents
189
+ ((CLEANUP_COUNTER++)) || true
190
+ if [[ $CLEANUP_COUNTER -ge $CLEANUP_INTERVAL ]]; then
191
+ cleanup_dead_agents
192
+ CLEANUP_COUNTER=0
193
+ fi
194
+ # Find all subscriber queue files
195
+ for queue_file in "$BUS_DIR/queues"/*/pending.jsonl; do
196
+ if [[ ! -f "$queue_file" ]]; then
197
+ continue
198
+ fi
199
+
200
+ # Extract subscriber from path: .ufoo/bus/queues/claude-code_abc123/pending.jsonl
201
+ dir_name="$(basename "$(dirname "$queue_file")")"
202
+ # Convert back to subscriber format: claude-code_abc123 -> claude-code:abc123
203
+ subscriber="${dir_name/_/:}"
204
+
205
+ # Get current count
206
+ if [[ -s "$queue_file" ]]; then
207
+ count="$(wc -l < "$queue_file" | tr -d ' ')"
208
+ else
209
+ count=0
210
+ fi
211
+
212
+ # Get last known count
213
+ last="$(get_last_count "$dir_name")"
214
+
215
+ # If count increased, inject /bus
216
+ if [[ "$count" -gt "$last" ]]; then
217
+ echo "[daemon] $(date '+%H:%M:%S') New message for $subscriber ($last -> $count)"
218
+
219
+ # Try to inject
220
+ if bash "$SCRIPT_DIR/bus-inject.sh" "$subscriber" 2>&1; then
221
+ echo "[daemon] Injected /bus into $subscriber"
222
+ else
223
+ echo "[daemon] Failed to inject (window not found or no permission)"
224
+ fi
225
+ fi
226
+
227
+ set_last_count "$dir_name" "$count"
228
+ done
229
+
230
+ sleep "$INTERVAL"
231
+ done