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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
|
|
6
|
+
skill_roots=()
|
|
7
|
+
if [[ -d "$repo_root/SKILLS" ]]; then
|
|
8
|
+
skill_roots+=("$repo_root/SKILLS")
|
|
9
|
+
fi
|
|
10
|
+
if [[ -d "$repo_root/modules" ]]; then
|
|
11
|
+
while IFS= read -r d; do
|
|
12
|
+
skill_roots+=("$d")
|
|
13
|
+
done < <(find "$repo_root/modules" -maxdepth 2 -type d -name SKILLS | sort)
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
usage() {
|
|
17
|
+
cat <<'EOFU'
|
|
18
|
+
skills
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
ufoo skills list
|
|
22
|
+
ufoo skills install <name|all> [--target <dir> | --codex | --agents]
|
|
23
|
+
EOFU
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
cmd="${1:-}"
|
|
27
|
+
shift || true
|
|
28
|
+
|
|
29
|
+
case "$cmd" in
|
|
30
|
+
list)
|
|
31
|
+
for root in "${skill_roots[@]}"; do
|
|
32
|
+
find "$root" -mindepth 1 -maxdepth 1 -type d -print
|
|
33
|
+
done | sed 's|.*/||' | sort -u
|
|
34
|
+
;;
|
|
35
|
+
install)
|
|
36
|
+
name="${1:-}"
|
|
37
|
+
shift || true
|
|
38
|
+
if [[ -z "$name" ]]; then
|
|
39
|
+
echo "FAIL: install requires <name|all>" >&2
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
target=""
|
|
44
|
+
while [[ $# -gt 0 ]]; do
|
|
45
|
+
case "$1" in
|
|
46
|
+
--target)
|
|
47
|
+
target="$2"; shift 2 ;;
|
|
48
|
+
--codex)
|
|
49
|
+
target="${CODEX_HOME:-$HOME/.codex}/skills"; shift ;;
|
|
50
|
+
--agents)
|
|
51
|
+
target="$HOME/.agents/skills"; shift ;;
|
|
52
|
+
*)
|
|
53
|
+
echo "Unknown option: $1" >&2
|
|
54
|
+
usage >&2
|
|
55
|
+
exit 1
|
|
56
|
+
;;
|
|
57
|
+
esac
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
if [[ -z "$target" ]]; then
|
|
61
|
+
echo "FAIL: specify --target/--codex/--agents" >&2
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
mkdir -p "$target"
|
|
66
|
+
|
|
67
|
+
find_skill() {
|
|
68
|
+
local skill_name="$1"
|
|
69
|
+
local matches=()
|
|
70
|
+
local root
|
|
71
|
+
for root in "${skill_roots[@]}"; do
|
|
72
|
+
if [[ -f "$root/$skill_name/SKILL.md" ]]; then
|
|
73
|
+
matches+=("$root/$skill_name")
|
|
74
|
+
fi
|
|
75
|
+
done
|
|
76
|
+
if (( ${#matches[@]} == 0 )); then
|
|
77
|
+
echo "FAIL: missing skill '$skill_name'" >&2
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
if (( ${#matches[@]} > 1 )); then
|
|
81
|
+
echo "FAIL: duplicate skill name '$skill_name' in:" >&2
|
|
82
|
+
printf ' - %s\n' "${matches[@]}" >&2
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
85
|
+
echo "${matches[0]}"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
install_one() {
|
|
89
|
+
local skill_name="$1"
|
|
90
|
+
local src_dir
|
|
91
|
+
src_dir="$(find_skill "$skill_name")"
|
|
92
|
+
local src_file="$src_dir/SKILL.md"
|
|
93
|
+
local dst_dir="$target/$skill_name"
|
|
94
|
+
local dst_file="$dst_dir/SKILL.md"
|
|
95
|
+
|
|
96
|
+
mkdir -p "$dst_dir"
|
|
97
|
+
cp "$src_file" "$dst_file"
|
|
98
|
+
echo "Installed: $skill_name"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if [[ "$name" == "all" ]]; then
|
|
102
|
+
while IFS= read -r d; do
|
|
103
|
+
install_one "$d"
|
|
104
|
+
done < <(for root in "${skill_roots[@]}"; do find "$root" -mindepth 1 -maxdepth 1 -type d -print; done | sed 's|.*/||' | sort -u)
|
|
105
|
+
else
|
|
106
|
+
install_one "$name"
|
|
107
|
+
fi
|
|
108
|
+
;;
|
|
109
|
+
*)
|
|
110
|
+
usage
|
|
111
|
+
exit 1
|
|
112
|
+
;;
|
|
113
|
+
esac
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [[ ! -d ".ufoo" ]]; then
|
|
5
|
+
echo "FAIL: .ufoo not found. Run: ufoo init" >&2
|
|
6
|
+
exit 1
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
10
|
+
|
|
11
|
+
if [[ -f "$repo_root/scripts/banner.sh" ]]; then
|
|
12
|
+
# shellcheck disable=SC1090
|
|
13
|
+
source "$repo_root/scripts/banner.sh"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
agent_type="claude"
|
|
17
|
+
session_id="${CLAUDE_SESSION_ID:-}"
|
|
18
|
+
if [[ -n "${CODEX_SESSION_ID:-}" ]]; then
|
|
19
|
+
agent_type="codex"
|
|
20
|
+
session_id="$CODEX_SESSION_ID"
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
subscriber=""
|
|
24
|
+
if [[ -f ".ufoo/bus/bus.json" ]]; then
|
|
25
|
+
cur_tty="$(tty 2>/dev/null || true)"
|
|
26
|
+
if [[ "$cur_tty" == /dev/* ]]; then
|
|
27
|
+
subscriber="$(
|
|
28
|
+
jq -r --arg tty "$cur_tty" \
|
|
29
|
+
'.subscribers | to_entries[] | select(.value.tty == $tty) | .key' \
|
|
30
|
+
.ufoo/bus/bus.json 2>/dev/null | head -1
|
|
31
|
+
)"
|
|
32
|
+
fi
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if [[ -z "$subscriber" && -n "$session_id" ]]; then
|
|
36
|
+
if [[ "$agent_type" == "codex" ]]; then
|
|
37
|
+
subscriber="codex:$session_id"
|
|
38
|
+
else
|
|
39
|
+
subscriber="claude-code:$session_id"
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if [[ -z "$session_id" && "$subscriber" == *:* ]]; then
|
|
44
|
+
session_id="${subscriber#*:}"
|
|
45
|
+
fi
|
|
46
|
+
if [[ -z "$session_id" ]]; then
|
|
47
|
+
session_id="unknown"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
if declare -F show_banner >/dev/null 2>&1; then
|
|
51
|
+
show_banner "$agent_type" "$session_id" "$subscriber"
|
|
52
|
+
else
|
|
53
|
+
echo "=== ufoo status ==="
|
|
54
|
+
echo "Agent: ${subscriber:-$agent_type}"
|
|
55
|
+
echo ""
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
echo "Project: $(pwd)"
|
|
59
|
+
|
|
60
|
+
unread_total=0
|
|
61
|
+
unread_lines=""
|
|
62
|
+
if [[ -d ".ufoo/bus/queues" ]]; then
|
|
63
|
+
shopt -s nullglob
|
|
64
|
+
for queue_file in .ufoo/bus/queues/*/pending.jsonl; do
|
|
65
|
+
[[ -s "$queue_file" ]] || continue
|
|
66
|
+
count=$(wc -l < "$queue_file" | tr -d ' ')
|
|
67
|
+
unread_total=$((unread_total + count))
|
|
68
|
+
safe_name="$(basename "$(dirname "$queue_file")")"
|
|
69
|
+
subscriber_name="$safe_name"
|
|
70
|
+
if [[ -f ".ufoo/bus/bus.json" ]]; then
|
|
71
|
+
subscriber_name="$(
|
|
72
|
+
jq -r --arg safe "$safe_name" \
|
|
73
|
+
'.subscribers | to_entries[] | select((.key|gsub(":";"_")) == $safe) | .key' \
|
|
74
|
+
.ufoo/bus/bus.json 2>/dev/null | head -1
|
|
75
|
+
)"
|
|
76
|
+
[[ -n "$subscriber_name" && "$subscriber_name" != "null" ]] || subscriber_name="$safe_name"
|
|
77
|
+
else
|
|
78
|
+
subscriber_name="${safe_name/_/:}"
|
|
79
|
+
fi
|
|
80
|
+
unread_lines+=$' - '"$subscriber_name"$': '"$count"$'\n'
|
|
81
|
+
done
|
|
82
|
+
shopt -u nullglob
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
echo "Unread messages: $unread_total"
|
|
86
|
+
if [[ -n "$unread_lines" ]]; then
|
|
87
|
+
printf "%s" "$unread_lines"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
decisions_dir=".ufoo/context/DECISIONS"
|
|
91
|
+
open_count=0
|
|
92
|
+
open_lines=""
|
|
93
|
+
|
|
94
|
+
get_status() {
|
|
95
|
+
local file="$1"
|
|
96
|
+
local status
|
|
97
|
+
if grep -q "^---$" "$file"; then
|
|
98
|
+
status=$(awk '/^---$/{if(++c==2)exit} c==1 && /^status:/{print $2}' "$file")
|
|
99
|
+
fi
|
|
100
|
+
echo "${status:-open}"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get_title() {
|
|
104
|
+
awk '/^#/{sub(/^# */,"");print; exit}' "$1"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if [[ -d "$decisions_dir" ]]; then
|
|
108
|
+
shopt -s nullglob
|
|
109
|
+
for f in "$decisions_dir"/*.md; do
|
|
110
|
+
[[ -f "$f" ]] || continue
|
|
111
|
+
status="$(get_status "$f")"
|
|
112
|
+
if [[ "$status" == "open" ]]; then
|
|
113
|
+
open_count=$((open_count + 1))
|
|
114
|
+
title="$(get_title "$f")"
|
|
115
|
+
[[ -z "$title" ]] && title="(no title)"
|
|
116
|
+
open_lines+=$' - '"$(basename "$f")"$': '"$title"$'\n'
|
|
117
|
+
fi
|
|
118
|
+
done
|
|
119
|
+
shopt -u nullglob
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
echo "Open decisions: $open_count"
|
|
123
|
+
if [[ -n "$open_lines" ]]; then
|
|
124
|
+
printf "%s" "$open_lines"
|
|
125
|
+
fi
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
const { spawn } = require("child_process");
|
|
2
|
+
const { randomUUID } = require("crypto");
|
|
3
|
+
|
|
4
|
+
function collectJsonl(text) {
|
|
5
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
|
6
|
+
const items = [];
|
|
7
|
+
for (const line of lines) {
|
|
8
|
+
try {
|
|
9
|
+
items.push(JSON.parse(line));
|
|
10
|
+
} catch {
|
|
11
|
+
// Ignore malformed lines
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return items;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function collectJson(text) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(text);
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runCommand(command, args, options = {}) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const child = spawn(command, args, {
|
|
28
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
29
|
+
...options,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
let stdout = "";
|
|
33
|
+
let stderr = "";
|
|
34
|
+
child.stdout.on("data", (d) => {
|
|
35
|
+
stdout += d.toString("utf8");
|
|
36
|
+
});
|
|
37
|
+
child.stderr.on("data", (d) => {
|
|
38
|
+
stderr += d.toString("utf8");
|
|
39
|
+
});
|
|
40
|
+
let timeout = null;
|
|
41
|
+
if (options.timeoutMs) {
|
|
42
|
+
timeout = setTimeout(() => {
|
|
43
|
+
try {
|
|
44
|
+
child.kill("SIGTERM");
|
|
45
|
+
} catch {
|
|
46
|
+
// ignore
|
|
47
|
+
}
|
|
48
|
+
reject(new Error("CLI timeout"));
|
|
49
|
+
}, options.timeoutMs);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
child.on("error", (err) => {
|
|
53
|
+
if (timeout) clearTimeout(timeout);
|
|
54
|
+
reject(err);
|
|
55
|
+
});
|
|
56
|
+
child.on("close", (code) => {
|
|
57
|
+
if (timeout) clearTimeout(timeout);
|
|
58
|
+
resolve({ code, stdout, stderr });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (options.input) {
|
|
62
|
+
child.stdin.write(options.input);
|
|
63
|
+
}
|
|
64
|
+
child.stdin.end();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const DEFAULT_CLAUDE = {
|
|
69
|
+
command: "claude",
|
|
70
|
+
args: [
|
|
71
|
+
"-p",
|
|
72
|
+
"--output-format",
|
|
73
|
+
"json",
|
|
74
|
+
"--dangerously-skip-permissions",
|
|
75
|
+
"--no-session-persistence",
|
|
76
|
+
"--json-schema",
|
|
77
|
+
'{"type":"object","properties":{"reply":{"type":"string"},"dispatch":{"type":"array","items":{"type":"object","properties":{"target":{"type":"string"},"message":{"type":"string"}},"required":["target","message"]}},"ops":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"agent":{"type":"string"},"count":{"type":"integer"},"agent_id":{"type":"string"},"nickname":{"type":"string"}},"required":["action"]}},"disambiguate":{"type":"object","properties":{"prompt":{"type":"string"},"candidates":{"type":"array","items":{"type":"object","properties":{"agent_id":{"type":"string"},"reason":{"type":"string"}},"required":["agent_id"]}}}}},"required":["reply","dispatch","ops"]}',
|
|
78
|
+
],
|
|
79
|
+
output: "json",
|
|
80
|
+
input: "arg",
|
|
81
|
+
modelArg: "--model",
|
|
82
|
+
sessionArg: "--session-id",
|
|
83
|
+
systemPromptArg: "--append-system-prompt",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const DEFAULT_CODEX = {
|
|
87
|
+
command: "codex",
|
|
88
|
+
args: ["exec", "--json", "--color", "never", "--sandbox", "read-only", "--skip-git-repo-check"],
|
|
89
|
+
output: "jsonl",
|
|
90
|
+
input: "arg",
|
|
91
|
+
modelArg: "--model",
|
|
92
|
+
sessionArg: null,
|
|
93
|
+
fallbackArgs: ["exec", "--json", "--color", "never", "--sandbox", "read-only"],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function buildArgs(backend, prompt, opts) {
|
|
97
|
+
const args = [...(backend.args || [])];
|
|
98
|
+
if (opts.model && backend.modelArg) {
|
|
99
|
+
args.push(backend.modelArg, opts.model);
|
|
100
|
+
}
|
|
101
|
+
if (opts.sessionId && backend.sessionArg && !opts.disableSession) {
|
|
102
|
+
args.push(backend.sessionArg, opts.sessionId);
|
|
103
|
+
}
|
|
104
|
+
if (opts.systemPrompt && backend.systemPromptArg) {
|
|
105
|
+
args.push(backend.systemPromptArg, opts.systemPrompt);
|
|
106
|
+
}
|
|
107
|
+
if (backend.input === "arg") {
|
|
108
|
+
args.push(prompt);
|
|
109
|
+
return { args, stdin: "" };
|
|
110
|
+
}
|
|
111
|
+
return { args, stdin: prompt };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isUnsupportedArgError(errText) {
|
|
115
|
+
const text = (errText || "").toLowerCase();
|
|
116
|
+
return text.includes("unknown option")
|
|
117
|
+
|| text.includes("unknown argument")
|
|
118
|
+
|| text.includes("unexpected argument")
|
|
119
|
+
|| text.includes("unrecognized option");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function runCliAgent(params) {
|
|
123
|
+
const backend = params.provider === "codex-cli" ? DEFAULT_CODEX : DEFAULT_CLAUDE;
|
|
124
|
+
const sessionId = params.sessionId || randomUUID();
|
|
125
|
+
const prompt =
|
|
126
|
+
params.systemPrompt && !backend.systemPromptArg
|
|
127
|
+
? `${params.systemPrompt}\n\n${params.prompt}`
|
|
128
|
+
: params.prompt;
|
|
129
|
+
const { args, stdin } = buildArgs(backend, prompt, {
|
|
130
|
+
model: params.model,
|
|
131
|
+
sessionId,
|
|
132
|
+
systemPrompt: params.systemPrompt,
|
|
133
|
+
disableSession: params.disableSession,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
let res;
|
|
137
|
+
const env = { ...process.env, ...(params.env || {}) };
|
|
138
|
+
delete env.CLAUDE_SESSION_ID;
|
|
139
|
+
delete env.CODEX_SESSION_ID;
|
|
140
|
+
try {
|
|
141
|
+
res = await runCommand(backend.command, args, {
|
|
142
|
+
cwd: params.cwd,
|
|
143
|
+
env,
|
|
144
|
+
input: stdin,
|
|
145
|
+
timeoutMs: params.timeoutMs || 300000, // 5 minutes for complex tasks
|
|
146
|
+
});
|
|
147
|
+
} catch (err) {
|
|
148
|
+
return { ok: false, error: err.message || String(err), sessionId };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (res.code !== 0) {
|
|
152
|
+
const err = res.stderr || res.stdout || "CLI failed";
|
|
153
|
+
if (backend === DEFAULT_CODEX && backend.fallbackArgs && isUnsupportedArgError(err)) {
|
|
154
|
+
const retry = buildArgs(
|
|
155
|
+
{ ...backend, args: backend.fallbackArgs },
|
|
156
|
+
prompt,
|
|
157
|
+
{
|
|
158
|
+
model: params.model,
|
|
159
|
+
sessionId,
|
|
160
|
+
systemPrompt: params.systemPrompt,
|
|
161
|
+
disableSession: params.disableSession,
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
try {
|
|
165
|
+
res = await runCommand(backend.command, retry.args, {
|
|
166
|
+
cwd: params.cwd,
|
|
167
|
+
env,
|
|
168
|
+
input: retry.stdin,
|
|
169
|
+
timeoutMs: params.timeoutMs || 60000,
|
|
170
|
+
});
|
|
171
|
+
} catch (err2) {
|
|
172
|
+
return { ok: false, error: err2.message || String(err2), sessionId };
|
|
173
|
+
}
|
|
174
|
+
if (res.code !== 0) {
|
|
175
|
+
const err2 = res.stderr || res.stdout || "CLI failed";
|
|
176
|
+
return { ok: false, error: err2, sessionId };
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
return { ok: false, error: err, sessionId };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (backend.output === "jsonl") {
|
|
184
|
+
return { ok: true, sessionId, output: collectJsonl(res.stdout) };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { ok: true, sessionId, output: collectJson(res.stdout) };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { runCliAgent };
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
const { randomBytes } = require("crypto");
|
|
5
|
+
const { runCliAgent } = require("./cliRunner");
|
|
6
|
+
const { normalizeCliOutput } = require("./normalizeOutput");
|
|
7
|
+
|
|
8
|
+
function sleep(ms) {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function generateSessionId() {
|
|
13
|
+
return randomBytes(4).toString("hex");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildEnv(agentType, sessionId, publisher, nickname) {
|
|
17
|
+
const env = { ...process.env };
|
|
18
|
+
if (agentType === "codex") {
|
|
19
|
+
env.CODEX_SESSION_ID = sessionId;
|
|
20
|
+
env.CLAUDE_SESSION_ID = "";
|
|
21
|
+
} else {
|
|
22
|
+
env.CLAUDE_SESSION_ID = sessionId;
|
|
23
|
+
env.CODEX_SESSION_ID = "";
|
|
24
|
+
}
|
|
25
|
+
env.AI_BUS_PUBLISHER = publisher || env.AI_BUS_PUBLISHER || "";
|
|
26
|
+
env.UFOO_NICKNAME = nickname || env.UFOO_NICKNAME || "";
|
|
27
|
+
env.UFOO_PARENT_PID = String(process.pid);
|
|
28
|
+
return env;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function joinBus(projectRoot, agentType, sessionId, nickname) {
|
|
32
|
+
const env = buildEnv(agentType, sessionId, "", nickname);
|
|
33
|
+
const args = ["bus", "join", sessionId, agentType === "codex" ? "codex" : "claude-code"];
|
|
34
|
+
if (nickname) args.push(nickname);
|
|
35
|
+
const res = spawnSync("ufoo", args, {
|
|
36
|
+
cwd: projectRoot,
|
|
37
|
+
env,
|
|
38
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
39
|
+
});
|
|
40
|
+
if (res.status !== 0) {
|
|
41
|
+
const err = (res.stderr || res.stdout || "").toString("utf8").trim();
|
|
42
|
+
throw new Error(err || "bus join failed");
|
|
43
|
+
}
|
|
44
|
+
const out = (res.stdout || "").toString("utf8").trim().split(/\r?\n/);
|
|
45
|
+
const subscriber = out[out.length - 1];
|
|
46
|
+
return { subscriber, env };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function safeSubscriber(subscriber) {
|
|
50
|
+
return subscriber.replace(/:/g, "_");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readQueue(queueFile) {
|
|
54
|
+
if (!fs.existsSync(queueFile)) return [];
|
|
55
|
+
try {
|
|
56
|
+
const content = fs.readFileSync(queueFile, "utf8");
|
|
57
|
+
if (!content.trim()) return [];
|
|
58
|
+
return content.split(/\r?\n/).filter(Boolean);
|
|
59
|
+
} catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function truncateQueue(queueFile) {
|
|
65
|
+
try {
|
|
66
|
+
fs.truncateSync(queueFile, 0);
|
|
67
|
+
} catch {
|
|
68
|
+
// ignore
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function handleEvent(projectRoot, agentType, provider, model, subscriber, sessionId, nickname, evt, cliSessionState) {
|
|
73
|
+
if (!evt || !evt.data || !evt.data.message) return;
|
|
74
|
+
const prompt = evt.data.message;
|
|
75
|
+
const publisher = evt.publisher || "unknown";
|
|
76
|
+
|
|
77
|
+
let res = await runCliAgent({
|
|
78
|
+
provider,
|
|
79
|
+
model,
|
|
80
|
+
prompt,
|
|
81
|
+
sessionId: cliSessionState.cliSessionId,
|
|
82
|
+
cwd: projectRoot,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Handle session errors with immediate retry (only for claude)
|
|
86
|
+
if (!res.ok && provider === "claude-cli") {
|
|
87
|
+
const errMsg = (res.error || "").toLowerCase();
|
|
88
|
+
if (errMsg.includes("session") || errMsg.includes("already in use")) {
|
|
89
|
+
// Clear session and retry immediately with new session
|
|
90
|
+
cliSessionState.cliSessionId = null;
|
|
91
|
+
cliSessionState.needsSave = true;
|
|
92
|
+
|
|
93
|
+
res = await runCliAgent({
|
|
94
|
+
provider,
|
|
95
|
+
model,
|
|
96
|
+
prompt,
|
|
97
|
+
sessionId: null, // Let runCliAgent generate new session
|
|
98
|
+
cwd: projectRoot,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Update CLI session ID for continuity (only for claude)
|
|
104
|
+
if (res.ok && res.sessionId && provider === "claude-cli") {
|
|
105
|
+
cliSessionState.cliSessionId = res.sessionId;
|
|
106
|
+
cliSessionState.needsSave = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let reply = "";
|
|
110
|
+
if (res.ok) {
|
|
111
|
+
reply = normalizeCliOutput(res.output) || "";
|
|
112
|
+
} else {
|
|
113
|
+
reply = `[internal:${agentType}] error: ${res.error || "unknown error"}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!reply) return;
|
|
117
|
+
|
|
118
|
+
spawnSync("ufoo", ["bus", "send", publisher, reply], {
|
|
119
|
+
cwd: projectRoot,
|
|
120
|
+
env: buildEnv(agentType, sessionId, subscriber, nickname),
|
|
121
|
+
stdio: "ignore",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function runInternalRunner({ projectRoot, agentType = "codex" }) {
|
|
126
|
+
const sessionId = generateSessionId();
|
|
127
|
+
const nickname = process.env.UFOO_NICKNAME || "";
|
|
128
|
+
const { subscriber } = joinBus(projectRoot, agentType, sessionId, nickname);
|
|
129
|
+
if (!subscriber) {
|
|
130
|
+
throw new Error("Failed to join bus for internal runner");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const queueDir = path.join(projectRoot, ".ufoo", "bus", "queues", safeSubscriber(subscriber));
|
|
134
|
+
const queueFile = path.join(queueDir, "pending.jsonl");
|
|
135
|
+
const provider = agentType === "codex" ? "codex-cli" : "claude-cli";
|
|
136
|
+
const model = process.env.UFOO_AGENT_MODEL || "";
|
|
137
|
+
|
|
138
|
+
// Session state management for CLI continuity
|
|
139
|
+
// Use stable path based on nickname (if exists) or agent type, NOT subscriber ID
|
|
140
|
+
const stableKey = nickname || `${agentType}-default`;
|
|
141
|
+
const sessionDir = path.join(projectRoot, ".ufoo", "agent", "sessions");
|
|
142
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
143
|
+
const stateFile = path.join(sessionDir, `${stableKey}.json`);
|
|
144
|
+
|
|
145
|
+
let cliSessionId = null;
|
|
146
|
+
// Only load session for claude (codex doesn't support sessions)
|
|
147
|
+
if (provider === "claude-cli") {
|
|
148
|
+
try {
|
|
149
|
+
const state = JSON.parse(fs.readFileSync(stateFile, "utf8"));
|
|
150
|
+
cliSessionId = state.cliSessionId;
|
|
151
|
+
} catch {
|
|
152
|
+
// No previous session
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let running = true;
|
|
157
|
+
let processing = false;
|
|
158
|
+
|
|
159
|
+
const stop = () => {
|
|
160
|
+
running = false;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
process.on("SIGTERM", stop);
|
|
164
|
+
process.on("SIGINT", stop);
|
|
165
|
+
|
|
166
|
+
const cliSessionState = { cliSessionId, needsSave: false };
|
|
167
|
+
|
|
168
|
+
while (running) {
|
|
169
|
+
if (!processing) {
|
|
170
|
+
processing = true;
|
|
171
|
+
try {
|
|
172
|
+
const lines = readQueue(queueFile);
|
|
173
|
+
if (lines.length > 0) {
|
|
174
|
+
const events = [];
|
|
175
|
+
for (const line of lines) {
|
|
176
|
+
try {
|
|
177
|
+
events.push(JSON.parse(line));
|
|
178
|
+
} catch {
|
|
179
|
+
// ignore malformed line
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
truncateQueue(queueFile);
|
|
183
|
+
|
|
184
|
+
for (const evt of events) {
|
|
185
|
+
// eslint-disable-next-line no-await-in-loop
|
|
186
|
+
await handleEvent(projectRoot, agentType, provider, model, subscriber, sessionId, nickname, evt, cliSessionState);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Persist CLI session state after processing (only if changed and for claude)
|
|
190
|
+
if (cliSessionState.needsSave && provider === "claude-cli") {
|
|
191
|
+
try {
|
|
192
|
+
fs.writeFileSync(stateFile, JSON.stringify({
|
|
193
|
+
cliSessionId: cliSessionState.cliSessionId,
|
|
194
|
+
nickname: nickname || "",
|
|
195
|
+
updated_at: new Date().toISOString(),
|
|
196
|
+
}));
|
|
197
|
+
cliSessionState.needsSave = false;
|
|
198
|
+
} catch {
|
|
199
|
+
// ignore save errors
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} finally {
|
|
204
|
+
processing = false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// eslint-disable-next-line no-await-in-loop
|
|
208
|
+
await sleep(1000);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = { runInternalRunner };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function extractTextFromObject(obj) {
|
|
2
|
+
if (!obj || typeof obj !== "object") return "";
|
|
3
|
+
if (obj.structured_output && typeof obj.structured_output === "object") {
|
|
4
|
+
return JSON.stringify(obj.structured_output);
|
|
5
|
+
}
|
|
6
|
+
const candidates = ["output", "text", "message", "content", "output_text", "result"];
|
|
7
|
+
for (const key of candidates) {
|
|
8
|
+
const val = obj[key];
|
|
9
|
+
if (typeof val === "string") return val;
|
|
10
|
+
}
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeCliOutput(output) {
|
|
15
|
+
if (!output) return "";
|
|
16
|
+
if (typeof output === "string") return output;
|
|
17
|
+
if (Array.isArray(output)) {
|
|
18
|
+
const parts = [];
|
|
19
|
+
for (const item of output) {
|
|
20
|
+
if (typeof item === "string") {
|
|
21
|
+
parts.push(item);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (item && typeof item === "object") {
|
|
25
|
+
if (item.item && typeof item.item === "object") {
|
|
26
|
+
if (item.item.type === "agent_message" && typeof item.item.text === "string") {
|
|
27
|
+
parts.push(item.item.text);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (typeof item.text === "string") parts.push(item.text);
|
|
32
|
+
else if (typeof item.content === "string") parts.push(item.content);
|
|
33
|
+
else if (typeof item.output === "string") parts.push(item.output);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return parts.join("\n").trim();
|
|
37
|
+
}
|
|
38
|
+
return extractTextFromObject(output);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { normalizeCliOutput };
|