Rename project to Constant and prepare public repo

This commit is contained in:
L'électron rare
2026-04-05 18:09:21 +02:00
parent fc0d3a67a6
commit a7a96ab057
32 changed files with 5561 additions and 225 deletions
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
repo_dir="$(cd "$script_dir/.." && pwd -P)"
default_python="$HOME/.local/share/constant/venv/bin/python3"
python_bin="${CONSTANT_PYTHON:-}"
if [[ -z "$python_bin" ]]; then
if [[ -x "$default_python" ]]; then
python_bin="$default_python"
else
python_bin="python3"
fi
fi
export PYTHONPATH="$repo_dir${PYTHONPATH+:$PYTHONPATH}"
exec "$python_bin" -m constant "$@"
+373
View File
@@ -0,0 +1,373 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
usage() {
cat <<EOF
Usage: $(basename "$0") <command> [options]
Commands:
send Relay one message to one machine
broadcast Relay one message to all machines
sync Refresh a local cache of remote inbox summaries
tail Poll all machines and print new summaries
Defaults:
repo-dir: $(zellij_ai_default_repo_dir)
session: $(zellij_ai_default_session)
EOF
}
repo_dir="$(zellij_ai_default_repo_dir)"
session="$(zellij_ai_default_session)"
cache_dir="$(zellij_ai_bridge_cache_dir)"
machines=()
default_machine_specs_array() {
local line
while IFS= read -r line; do
[[ -z "$line" ]] && continue
printf '%s\n' "$line"
done < <(zellij_ai_default_machine_specs)
}
load_machines() {
if [[ ${#machines[@]} -eq 0 ]]; then
mapfile -t machines < <(default_machine_specs_array)
fi
}
resolve_machine() {
local needle="$1"
local resolved
if ! resolved="$(zellij_ai_resolve_machine_spec "$needle")"; then
echo "Unknown machine: $needle" >&2
exit 2
fi
printf '%s\n' "$resolved"
}
run_ai_msg() {
local label="$1"
local target="$2"
shift 2
if zellij_ai_is_local_target "$target"; then
ZELLIJ_AI_MACHINE_NAME="$label" \
ZELLIJ_AI_SESSION="$session" \
"$(zellij_ai_expand_home_path "$repo_dir")/scripts/ai-msg.sh" "$@"
return 0
fi
ssh "$target" bash -s -- "$label" "$session" "$repo_dir" "$@" <<'EOF'
set -euo pipefail
label="$1"
shift
session="$1"
shift
repo_dir_input="$1"
shift
expand_home_path() {
case "$1" in
'$HOME'|'$HOME/'*)
printf '%s\n' "${HOME}${1#\$HOME}"
;;
"~")
printf '%s\n' "$HOME"
;;
"~/"*)
printf '%s\n' "$HOME/${1#~/}"
;;
*)
printf '%s\n' "$1"
;;
esac
}
repo_dir="$(expand_home_path "$repo_dir_input")"
export ZELLIJ_AI_MACHINE_NAME="$label"
export ZELLIJ_AI_SESSION="$session"
exec "$repo_dir/scripts/ai-msg.sh" "$@"
EOF
}
sync_once() {
local machine_spec resolved label target cache_file
mkdir -p "$cache_dir"
load_machines
for machine_spec in "${machines[@]}"; do
IFS=$'\t' read -r label target <<<"$(resolve_machine "$machine_spec")"
cache_file="$cache_dir/${label}.tsv"
run_ai_msg "$label" "$target" list --for all >"$cache_file"
done
}
command_send() {
local from_machine="$(zellij_ai_local_machine_label)"
local from_role="bridge"
local to_machine=""
local to_role=""
local message_text=""
local message_file=""
local resolved label target
while [[ $# -gt 0 ]]; do
case "$1" in
--from-machine)
from_machine="$2"
shift 2
;;
--from)
from_role="$2"
shift 2
;;
--to-machine)
to_machine="$2"
shift 2
;;
--to)
to_role="$2"
shift 2
;;
--message)
message_text="$2"
shift 2
;;
--file)
message_file="$2"
shift 2
;;
--machine)
machines+=("$2")
shift 2
;;
--repo-dir)
repo_dir="$2"
shift 2
;;
--session)
session="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option for send: $1" >&2
exit 2
;;
esac
done
if [[ -z "$to_machine" || -z "$to_role" ]]; then
echo "--to-machine and --to are required." >&2
exit 2
fi
IFS=$'\t' read -r label target <<<"$(resolve_machine "$to_machine")"
if [[ -n "$message_text" ]]; then
run_ai_msg "$label" "$target" send --from "$from_role" --to "$to_role" --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)" --message "$message_text"
elif [[ -n "$message_file" ]]; then
run_ai_msg "$label" "$target" send --from "$from_role" --to "$to_role" --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)" --file "$message_file"
elif [[ ! -t 0 ]]; then
run_ai_msg "$label" "$target" send --from "$from_role" --to "$to_role" --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)"
else
echo "Provide --message, --file, or stdin." >&2
exit 2
fi
}
command_broadcast() {
local from_machine="$(zellij_ai_local_machine_label)"
local from_role="bridge"
local message_text=""
local message_file=""
local machine_spec resolved label target
while [[ $# -gt 0 ]]; do
case "$1" in
--from-machine)
from_machine="$2"
shift 2
;;
--from)
from_role="$2"
shift 2
;;
--message)
message_text="$2"
shift 2
;;
--file)
message_file="$2"
shift 2
;;
--machine)
machines+=("$2")
shift 2
;;
--repo-dir)
repo_dir="$2"
shift 2
;;
--session)
session="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option for broadcast: $1" >&2
exit 2
;;
esac
done
load_machines
for machine_spec in "${machines[@]}"; do
IFS=$'\t' read -r label target <<<"$(resolve_machine "$machine_spec")"
if [[ -n "$message_text" ]]; then
run_ai_msg "$label" "$target" send --from "$from_role" --to all --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)" --message "$message_text"
elif [[ -n "$message_file" ]]; then
run_ai_msg "$label" "$target" send --from "$from_role" --to all --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)" --file "$message_file"
elif [[ ! -t 0 ]]; then
run_ai_msg "$label" "$target" send --from "$from_role" --to all --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)"
else
echo "Provide --message, --file, or stdin." >&2
exit 2
fi
done
}
command_sync() {
while [[ $# -gt 0 ]]; do
case "$1" in
--machine)
machines+=("$2")
shift 2
;;
--repo-dir)
repo_dir="$2"
shift 2
;;
--session)
session="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option for sync: $1" >&2
exit 2
;;
esac
done
sync_once
find "$cache_dir" -type f -name '*.tsv' | sort | while IFS= read -r file; do
cat "$file"
done
}
command_tail() {
local interval=3
declare -A seen=()
while [[ $# -gt 0 ]]; do
case "$1" in
--machine)
machines+=("$2")
shift 2
;;
--repo-dir)
repo_dir="$2"
shift 2
;;
--session)
session="$2"
shift 2
;;
--interval)
interval="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option for tail: $1" >&2
exit 2
;;
esac
done
while true; do
sync_once
while IFS= read -r file; do
while IFS= read -r line; do
[[ -z "$line" ]] && continue
if [[ -n "${seen[$line]:-}" ]]; then
continue
fi
seen["$line"]=1
printf '%s\n' "$line"
done <"$file"
done < <(find "$cache_dir" -type f -name '*.tsv' | sort)
sleep "$interval"
done
}
if [[ $# -lt 1 ]]; then
usage >&2
exit 2
fi
command="$1"
shift
case "$command" in
send)
command_send "$@"
;;
broadcast)
command_broadcast "$@"
;;
sync)
command_sync "$@"
;;
tail)
command_tail "$@"
;;
-h|--help|help)
usage
;;
*)
echo "Unknown command: $command" >&2
usage >&2
exit 2
;;
esac
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: ai-copy.sh [options]
Copy text into the macOS clipboard on the current machine.
Options:
--text TEXT Copy this text directly
--file FILE Copy the contents of FILE
-h, --help Show this help
If neither --text nor --file is provided, stdin is copied.
EOF
}
text=""
file=""
while [[ $# -gt 0 ]]; do
case "$1" in
--text)
text="$2"
shift 2
;;
--file)
file="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
if [[ -n "$text" && -n "$file" ]]; then
echo "Use either --text or --file, not both." >&2
exit 2
fi
if ! command -v pbcopy >/dev/null 2>&1; then
echo "pbcopy is not available on this machine." >&2
exit 1
fi
if [[ -n "$text" ]]; then
printf '%s' "$text" | pbcopy
elif [[ -n "$file" ]]; then
cat "$file" | pbcopy
elif [[ ! -t 0 ]]; then
cat | pbcopy
else
echo "Nothing to copy. Provide --text, --file, or pipe stdin." >&2
exit 2
fi
printf 'Copied to the macOS clipboard.\n'
+376
View File
@@ -0,0 +1,376 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
usage() {
cat <<'EOF'
Usage: ai-msg.sh <command> [options]
Commands:
send Send a message to one role
broadcast Send a message to all roles on the current machine
inbox Show messages addressed to one role
tail Poll and print new messages for one role
list List matching messages in TSV form
Environment defaults:
ZELLIJ_AI_BUS_DIR
ZELLIJ_AI_MACHINE_NAME
ZELLIJ_AI_ROLE
ZELLIJ_AI_SESSION
EOF
}
machine_name="$(zellij_ai_current_machine_name)"
session="${ZELLIJ_AI_SESSION:-$(zellij_ai_default_session)}"
role_default="${ZELLIJ_AI_ROLE:-all}"
bus_dir="${ZELLIJ_AI_BUS_DIR:-$(zellij_ai_session_state_dir "$session")/bus}"
messages_dir="${bus_dir}/messages"
ensure_messages_dir() {
mkdir -p "$messages_dir"
}
random_suffix() {
od -An -N4 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n' || printf '%s' "$$"
}
message_id() {
printf '%s\n' "${machine_name}-${session}-$(date -u +%Y%m%dT%H%M%SZ)-$$-$(random_suffix)"
}
timestamp_utc() {
date -u +"%Y-%m-%dT%H:%M:%SZ"
}
read_body_input() {
local message_text="$1"
local message_file="$2"
if [[ -n "$message_text" ]]; then
printf '%s' "$message_text"
return 0
fi
if [[ -n "$message_file" ]]; then
cat "$message_file"
return 0
fi
if [[ ! -t 0 ]]; then
cat
return 0
fi
echo "No message body provided. Use --message, --file, or stdin." >&2
return 1
}
read_header() {
local key="$1"
local file="$2"
sed -n "s/^${key}: //p" "$file" | head -n 1
}
read_body() {
awk 'seen_blank { print } /^$/ { seen_blank = 1 }' "$1"
}
message_matches_role() {
local file="$1"
local role="$2"
local target
target="$(read_header target "$file")"
if [[ "$role" == "all" ]]; then
return 0
fi
[[ "$target" == "$role" || "$target" == "all" ]]
}
list_message_files() {
if [[ ! -d "$messages_dir" ]]; then
return 0
fi
find "$messages_dir" -type f -name '*.msg' | sort
}
print_message_tsv() {
local file="$1"
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"$(read_header id "$file")" \
"$(read_header timestamp "$file")" \
"$(read_header machine "$file")" \
"$(read_header session "$file")" \
"$(read_header sender "$file")" \
"$(read_header target "$file")" \
"$(read_header source_machine "$file")" \
"$(read_header source_role "$file")" \
"$file"
}
print_message_pretty() {
local file="$1"
local id timestamp sender target stored_machine source_machine source_role relay_via
id="$(read_header id "$file")"
timestamp="$(read_header timestamp "$file")"
sender="$(read_header sender "$file")"
target="$(read_header target "$file")"
stored_machine="$(read_header machine "$file")"
source_machine="$(read_header source_machine "$file")"
source_role="$(read_header source_role "$file")"
relay_via="$(read_header relay_via "$file")"
printf '[%s] %s -> %s on %s (%s)\n' "$timestamp" "$sender" "$target" "$stored_machine" "$id"
if [[ -n "$source_machine" || -n "$source_role" ]]; then
printf 'source: %s/%s\n' "${source_machine:-unknown}" "${source_role:-unknown}"
fi
if [[ -n "$relay_via" ]]; then
printf 'relay: %s\n' "$relay_via"
fi
printf '%s\n' '---'
read_body "$file"
printf '\n'
}
write_message_file() {
local sender="$1"
local target="$2"
local source_machine="$3"
local source_role="$4"
local relay_via="$5"
local body="$6"
local id timestamp file tmp_file
ensure_messages_dir
id="$(message_id)"
timestamp="$(timestamp_utc)"
file="${messages_dir}/${id}.msg"
tmp_file="$(mktemp "${messages_dir}/.tmp.XXXXXX")"
{
printf 'id: %s\n' "$id"
printf 'timestamp: %s\n' "$timestamp"
printf 'machine: %s\n' "$machine_name"
printf 'session: %s\n' "$session"
printf 'sender: %s\n' "$sender"
printf 'target: %s\n' "$target"
printf 'source_machine: %s\n' "$source_machine"
printf 'source_role: %s\n' "$source_role"
printf 'relay_via: %s\n' "$relay_via"
printf '\n'
printf '%s\n' "$body"
} >"$tmp_file"
mv "$tmp_file" "$file"
printf '%s\n' "$file"
}
command_send() {
local sender="$role_default"
local target=""
local message_text=""
local message_file=""
local source_machine=""
local source_role=""
local relay_via=""
local body file
while [[ $# -gt 0 ]]; do
case "$1" in
--from)
sender="$2"
shift 2
;;
--to)
target="$2"
shift 2
;;
--message)
message_text="$2"
shift 2
;;
--file)
message_file="$2"
shift 2
;;
--source-machine)
source_machine="$2"
shift 2
;;
--source-role)
source_role="$2"
shift 2
;;
--relay-via)
relay_via="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option for send: $1" >&2
exit 2
;;
esac
done
if [[ -z "$target" ]]; then
echo "--to is required for send." >&2
exit 2
fi
body="$(read_body_input "$message_text" "$message_file")"
file="$(write_message_file "$sender" "$target" "$source_machine" "$source_role" "$relay_via" "$body")"
printf 'stored: %s\n' "$file"
}
command_broadcast() {
command_send --to all "$@"
}
command_list() {
local role="all"
while [[ $# -gt 0 ]]; do
case "$1" in
--for)
role="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option for list: $1" >&2
exit 2
;;
esac
done
while IFS= read -r file; do
message_matches_role "$file" "$role" || continue
print_message_tsv "$file"
done < <(list_message_files)
}
command_inbox() {
local role="$role_default"
local found=false
while [[ $# -gt 0 ]]; do
case "$1" in
--for)
role="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option for inbox: $1" >&2
exit 2
;;
esac
done
while IFS= read -r file; do
message_matches_role "$file" "$role" || continue
print_message_pretty "$file"
found=true
done < <(list_message_files)
if ! $found; then
printf 'No messages for %s in %s\n' "$role" "$messages_dir"
fi
}
command_tail() {
local role="$role_default"
local interval=2
declare -A seen=()
while [[ $# -gt 0 ]]; do
case "$1" in
--for)
role="$2"
shift 2
;;
--interval)
interval="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option for tail: $1" >&2
exit 2
;;
esac
done
while true; do
while IFS= read -r file; do
message_matches_role "$file" "$role" || continue
if [[ -n "${seen[$file]:-}" ]]; then
continue
fi
seen["$file"]=1
print_message_pretty "$file"
done < <(list_message_files)
sleep "$interval"
done
}
if [[ $# -lt 1 ]]; then
usage >&2
exit 2
fi
command="$1"
shift
case "$command" in
send)
command_send "$@"
;;
broadcast)
command_broadcast "$@"
;;
list)
command_list "$@"
;;
inbox)
command_inbox "$@"
;;
tail)
command_tail "$@"
;;
-h|--help|help)
usage
;;
*)
echo "Unknown command: $command" >&2
usage >&2
exit 2
;;
esac
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
export CONSTANT_SCRIPT_NAME="$(basename "$0")"
exec "$script_dir/zellij-ai-fleet-install.sh" "$@"
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
export CONSTANT_SCRIPT_NAME="$(basename "$0")"
exec "$script_dir/zellij-ai-fleet.sh" "$@"
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
export CONSTANT_SCRIPT_NAME="$(basename "$0")"
exec "$script_dir/zellij-ai-triple.sh" "$@"
+19 -7
View File
@@ -1,7 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
bootstrap_file="${ZELLIJ_AI_BOOTSTRAP_FILE:-}"
workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}"
@@ -12,17 +18,23 @@ fi
if [[ -n "$bootstrap_file" && ! -f "$bootstrap_file" ]]; then
: >"$bootstrap_file"
printf -v codex1_cmd '%q %q' "$script_dir/zellij-ai-codex-pane.sh" "1"
printf -v codex2_cmd '%q %q' "$script_dir/zellij-ai-codex-pane.sh" "2"
printf -v codex_cmd '%q' "$script_dir/zellij-ai-codex-pane.sh"
printf -v copilot_cmd '%q' "$script_dir/zellij-ai-copilot-pane.sh"
printf -v vibe_cmd '%q' "$script_dir/zellij-ai-vibe-pane.sh"
zellij action new-pane -d right -n "${ZELLIJ_AI_CODEX1_LABEL:-codex-1}" --cwd "$workspace"
zellij action new-pane -d right -n "${ZELLIJ_AI_CODEX_LABEL:-codex}" --cwd "$workspace"
zellij action move-focus right
zellij action write-chars "$codex1_cmd"
zellij action write-chars "$codex_cmd"
zellij action write 10
zellij action new-pane -d down -n "${ZELLIJ_AI_CODEX2_LABEL:-codex-2}" --cwd "$workspace"
zellij action new-pane -d down -n "${ZELLIJ_AI_COPILOT_LABEL:-copilot}" --cwd "$workspace"
zellij action move-focus down
zellij action write-chars "$codex2_cmd"
zellij action write-chars "$copilot_cmd"
zellij action write 10
zellij action new-pane -d down -n "${ZELLIJ_AI_VIBE_LABEL:-vibe}" --cwd "$workspace"
zellij action move-focus down
zellij action write-chars "$vibe_cmd"
zellij action write 10
zellij action move-focus left
+29
View File
@@ -1,7 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}"
machine_name="${ZELLIJ_AI_MACHINE_NAME:-unknown}"
session_name="${ZELLIJ_AI_SESSION:-$(zellij_ai_default_session)}"
repo_dir="${ZELLIJ_AI_REPO_DIR:-}"
bus_dir="${ZELLIJ_AI_BUS_DIR:-$(zellij_ai_session_state_dir "$session_name")/bus}"
if [[ ! -d "$workspace" ]]; then
echo "Workspace not found: $workspace" >&2
@@ -12,10 +25,26 @@ if [[ -n "${ZELLIJ_AI_CLAUDE_CONFIG_DIR:-}" ]]; then
export CLAUDE_CONFIG_DIR="$ZELLIJ_AI_CLAUDE_CONFIG_DIR"
fi
export PATH="$(zellij_ai_agent_path)"
if [[ -n "$repo_dir" && -d "$repo_dir/scripts" ]]; then
export PATH="$repo_dir/scripts:$PATH"
fi
export ZELLIJ_AI_ROLE="claude"
export ZELLIJ_AI_MACHINE_NAME="$machine_name"
export ZELLIJ_AI_SESSION="$session_name"
export ZELLIJ_AI_BUS_DIR="$bus_dir"
cd "$workspace"
echo "Claude pane"
echo "machine: $machine_name"
echo "workspace: $workspace"
echo "bus: $bus_dir"
if [[ -n "$repo_dir" ]]; then
echo "helper: $repo_dir/scripts/ai-msg.sh"
fi
if [[ -n "${CLAUDE_CONFIG_DIR:-}" ]]; then
echo "config: $CLAUDE_CONFIG_DIR"
fi
+72 -121
View File
@@ -1,160 +1,111 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
usage() {
cat <<'EOF'
Usage: zellij-ai-codex-pane.sh <slot>
Usage: zellij-ai-codex-pane.sh [slot]
slot:
1 Launch the first Codex container/profile
2 Launch the second Codex container/profile
1 Deprecated alias for the single host-local Codex pane
2 Deprecated alias for the single host-local Codex pane
EOF
}
find_codex_binary() {
if [[ -n "${ZELLIJ_AI_CODEX_BINARY:-}" && -x "${ZELLIJ_AI_CODEX_BINARY}" ]]; then
printf '%s\n' "${ZELLIJ_AI_CODEX_BINARY}"
return 0
fi
if [[ -x "$HOME/.npm-global/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex" ]]; then
printf '%s\n' "$HOME/.npm-global/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex"
return 0
fi
if [[ -n "$(command -v codex || true)" ]]; then
local wrapper_dir package_dir candidate
wrapper_dir="$(cd "$(dirname "$(command -v codex)")" && pwd -P)"
package_dir="$(cd "$wrapper_dir/../lib/node_modules/@openai/codex/node_modules/@openai" 2>/dev/null && pwd -P || true)"
if [[ -n "$package_dir" ]]; then
candidate="$(find "$package_dir" -path '*/vendor/*/codex/codex' -type f -print -quit 2>/dev/null || true)"
if [[ -n "$candidate" && -x "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
fi
fi
return 1
warn_deprecated() {
printf 'Warning: %s\n' "$*" >&2
}
if [[ $# -ne 1 ]]; then
if [[ $# -gt 1 ]]; then
usage >&2
exit 2
fi
slot="$1"
case "$slot" in
1)
label="${ZELLIJ_AI_CODEX1_LABEL:-codex-1}"
profile_dir="${ZELLIJ_AI_CODEX1_HOME:-$HOME/.codex-profiles/codex-1}"
;;
2)
label="${ZELLIJ_AI_CODEX2_LABEL:-codex-2}"
profile_dir="${ZELLIJ_AI_CODEX2_HOME:-$HOME/.codex-profiles/codex-2}"
;;
*)
usage >&2
exit 2
;;
esac
if [[ $# -eq 1 ]]; then
case "$1" in
1)
warn_deprecated "slot 1 is deprecated; Codex now runs as a single host-local pane."
;;
2)
warn_deprecated "slot 2 is deprecated; launching the single host-local Codex pane."
;;
-h|--help|help)
usage
exit 0
;;
*)
usage >&2
exit 2
;;
esac
fi
workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}"
image="${ZELLIJ_AI_CODEX_IMAGE:-codercom/code-server:latest}"
profile_dir="${ZELLIJ_AI_CODEX_HOME:-$(zellij_ai_default_codex_home)}"
label="${ZELLIJ_AI_CODEX_LABEL:-codex}"
main_codex_config="${ZELLIJ_AI_MAIN_CODEX_CONFIG:-$HOME/.codex/config.toml}"
codex_binary="$(find_codex_binary || true)"
term_value="${ZELLIJ_AI_CODEX_TERM:-${TERM:-xterm-256color}}"
color_term_value="${COLORTERM:-truecolor}"
if [[ "$term_value" == "dumb" ]]; then
term_value="xterm-256color"
fi
repo_dir="${ZELLIJ_AI_REPO_DIR:-$(cd "$script_dir/.." && pwd -P)}"
session_name="${ZELLIJ_AI_SESSION:-$(zellij_ai_default_session)}"
bus_dir="${ZELLIJ_AI_BUS_DIR:-$(zellij_ai_session_state_dir "$session_name")/bus}"
machine_name="$(zellij_ai_current_machine_name)"
agent_path="$(zellij_ai_agent_path)"
if [[ ! -d "$workspace" ]]; then
echo "Workspace not found: $workspace" >&2
exit 1
fi
if [[ -z "$codex_binary" || ! -x "$codex_binary" ]]; then
echo "Unable to locate the native Codex binary on the host." >&2
echo "Set ZELLIJ_AI_CODEX_BINARY if Codex is installed elsewhere." >&2
export PATH="$agent_path"
if ! command -v codex >/dev/null 2>&1; then
echo "Codex CLI not found on the host." >&2
echo "Install it with: npm install -g @openai/codex" >&2
exit 1
fi
platform_dir="$(dirname "$(dirname "$codex_binary")")"
rg_binary="$platform_dir/path/rg"
if [[ ! -x "$rg_binary" ]]; then
echo "Bundled rg binary not found next to Codex: $rg_binary" >&2
exit 1
fi
if ! docker image inspect "$image" >/dev/null 2>&1; then
echo "Docker image not found locally: $image" >&2
echo "Pull or build the image first, or relaunch with --codex-image." >&2
exit 1
fi
mkdir -p "$profile_dir"
profile_dir="$(mkdir -p "$(zellij_ai_expand_home_path "$profile_dir")" && cd "$(zellij_ai_expand_home_path "$profile_dir")" && pwd -P)"
mkdir -p "$bus_dir/messages"
if [[ ! -f "$profile_dir/config.toml" && -f "$main_codex_config" ]]; then
cp "$main_codex_config" "$profile_dir/config.toml"
fi
container_name="zellij-$(printf '%s' "$label" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]._-' '-')"
uid="$(id -u)"
gid="$(id -g)"
if [[ -d "$repo_dir/scripts" ]]; then
export PATH="$repo_dir/scripts:$PATH"
fi
read -r -d '' inner_script <<EOF || true
set -euo pipefail
cd /workspace
export HOME=/codex-home
export CODEX_HOME=/codex-home
echo "Codex pane: $label"
echo "workspace: /workspace"
echo "profile: /codex-home"
echo "image: $image"
export CODEX_HOME="$profile_dir"
export ZELLIJ_AI_ROLE="$label"
export ZELLIJ_AI_MACHINE_NAME="$machine_name"
export ZELLIJ_AI_SESSION="$session_name"
export ZELLIJ_AI_BUS_DIR="$bus_dir"
cd "$workspace"
echo "Codex pane"
echo "machine: $machine_name"
echo "workspace: $workspace"
echo "profile: $profile_dir"
echo "bus: $bus_dir"
if [[ -n "$repo_dir" ]]; then
echo "helper: $repo_dir/scripts/ai-msg.sh"
fi
echo
if [ ! -f /codex-home/auth.json ]; then
echo "No auth.json found for $label."
echo "Starting: codex login --device-auth"
echo "Use a different ChatGPT/OpenAI account here than in the other pane."
echo
codex login --device-auth
echo
fi
exec codex --no-alt-screen -C /workspace
EOF
docker_args=(
run
--rm
--init
-it
--name "$container_name"
--hostname "$container_name"
--user "${uid}:${gid}"
--workdir /workspace
--entrypoint bash
-e HOME=/codex-home
-e CODEX_HOME=/codex-home
-e USER="${USER:-$(id -un)}"
-e TERM="$term_value"
-e COLORTERM="$color_term_value"
-e LANG="${LANG:-C.UTF-8}"
-v "${workspace}:/workspace"
-v "${profile_dir}:/codex-home"
-v "${codex_binary}:/usr/local/bin/codex:ro"
-v "${rg_binary}:/usr/local/bin/rg:ro"
)
if [[ -f "$HOME/.gitconfig" ]]; then
docker_args+=(-v "$HOME/.gitconfig:/codex-home/.gitconfig:ro")
if [[ ! -f "$CODEX_HOME/auth.json" ]]; then
echo "No auth.json found for $label."
echo "Starting: codex login --device-auth"
echo
codex login --device-auth
echo
fi
docker_args+=("$image" -lc "$inner_script")
exec docker "${docker_args[@]}"
exec codex --no-alt-screen -C "$workspace"
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env bash
zellij_ai_default_session() {
printf '%s\n' "constant"
}
zellij_ai_default_local_session() {
printf '%s\n' "constant-fleet"
}
zellij_ai_default_repo_dir() {
printf '%s\n' '$HOME/constant'
}
zellij_ai_default_codex_home() {
printf '%s\n' '$HOME/.codex-profiles/codex'
}
zellij_ai_default_codex_image() {
printf '%s\n' "codercom/code-server:latest"
}
zellij_ai_runtime_root() {
printf '%s\n' "${ZELLIJ_AI_RUNTIME_ROOT:-$HOME/.cache/constant/zellij}"
}
zellij_ai_session_state_dir() {
printf '%s\n' "$(zellij_ai_runtime_root)/$1"
}
zellij_ai_fleet_state_dir() {
printf '%s\n' "$(zellij_ai_runtime_root)/$1-fleet"
}
zellij_ai_bridge_cache_dir() {
printf '%s\n' "$(zellij_ai_runtime_root)/bridge-cache"
}
zellij_ai_default_machine_specs() {
cat <<'EOF'
command-center=local
builder-a=dev@builder-a
builder-b=dev@builder-b
edge-a=dev@edge-a
lab-a=dev@lab-a
EOF
}
zellij_ai_strip_ansi() {
sed -E 's/\x1b\[[0-9;]*m//g'
}
zellij_ai_session_exists() {
zellij list-sessions 2>/dev/null | zellij_ai_strip_ansi | awk '{print $1}' | grep -Fxq "$1"
}
zellij_ai_kdl_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
zellij_ai_expand_home_path() {
case "$1" in
'$HOME'|'$HOME/'*)
printf '%s\n' "${HOME}${1#\$HOME}"
;;
"~")
printf '%s\n' "$HOME"
;;
"~/"*)
printf '%s\n' "$HOME/${1#~/}"
;;
*)
printf '%s\n' "$1"
;;
esac
}
zellij_ai_expand_home_path_for_home() {
local value="$1"
local target_home="$2"
case "$value" in
'$HOME'|'$HOME/'*)
printf '%s\n' "${target_home}${value#\$HOME}"
;;
"~")
printf '%s\n' "$target_home"
;;
"~/"*)
printf '%s\n' "$target_home/${value#~/}"
;;
*)
printf '%s\n' "$value"
;;
esac
}
zellij_ai_is_local_target() {
local value="$1"
local current_full current_short
current_full="$(hostname || true)"
current_short="$(hostname -s 2>/dev/null || printf '%s' "$current_full")"
case "$value" in
local|localhost|127.0.0.1|::1|"${current_full}"|"${current_short}")
return 0
;;
*)
return 1
;;
esac
}
zellij_ai_parse_machine_spec() {
local spec="$1"
local label target
if [[ "$spec" == *"="* ]]; then
label="${spec%%=*}"
target="${spec#*=}"
else
label="$spec"
target="$spec"
fi
if [[ -z "$label" || -z "$target" ]]; then
return 1
fi
printf '%s\t%s\n' "$label" "$target"
}
zellij_ai_resolve_machine_spec() {
local needle="$1"
local line label target
while IFS= read -r line; do
[[ -z "$line" ]] && continue
IFS=$'\t' read -r label target <<<"$(zellij_ai_parse_machine_spec "$line")" || continue
if [[ "$needle" == "$label" || "$needle" == "$target" || "$needle" == "$line" ]]; then
printf '%s\t%s\n' "$label" "$target"
return 0
fi
done < <(zellij_ai_default_machine_specs)
if IFS=$'\t' read -r label target <<<"$(zellij_ai_parse_machine_spec "$needle" 2>/dev/null)"; then
printf '%s\t%s\n' "$label" "$target"
return 0
fi
return 1
}
zellij_ai_require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Required command not found: $1" >&2
return 1
fi
}
zellij_ai_current_machine_name() {
if [[ -n "${ZELLIJ_AI_MACHINE_NAME:-}" ]]; then
printf '%s\n' "${ZELLIJ_AI_MACHINE_NAME}"
else
hostname -s 2>/dev/null || hostname || printf 'local'
fi
}
zellij_ai_local_machine_label() {
printf '%s\n' "command-center"
}
zellij_ai_agent_path() {
local path_value="$PATH"
if [[ -d "$HOME/.local/bin" ]]; then
path_value="$HOME/.local/bin:$path_value"
fi
if [[ -d "$HOME/.npm-global/bin" ]]; then
path_value="$HOME/.npm-global/bin:$path_value"
fi
printf '%s\n' "$path_value"
}
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}"
machine_name="${ZELLIJ_AI_MACHINE_NAME:-unknown}"
session_name="${ZELLIJ_AI_SESSION:-$(zellij_ai_default_session)}"
repo_dir="${ZELLIJ_AI_REPO_DIR:-$(cd "$script_dir/.." && pwd -P)}"
bus_dir="${ZELLIJ_AI_BUS_DIR:-$(zellij_ai_session_state_dir "$session_name")/bus}"
label="${ZELLIJ_AI_COPILOT_LABEL:-copilot}"
if [[ ! -d "$workspace" ]]; then
echo "Workspace not found: $workspace" >&2
exit 1
fi
export PATH="$(zellij_ai_agent_path)"
if [[ -n "$repo_dir" && -d "$repo_dir/scripts" ]]; then
export PATH="$repo_dir/scripts:$PATH"
fi
if ! command -v copilot >/dev/null 2>&1; then
echo "GitHub Copilot CLI not found on the host." >&2
echo "Install it with: npm install -g @github/copilot" >&2
exit 1
fi
mkdir -p "$bus_dir/messages"
export ZELLIJ_AI_ROLE="$label"
export ZELLIJ_AI_MACHINE_NAME="$machine_name"
export ZELLIJ_AI_SESSION="$session_name"
export ZELLIJ_AI_BUS_DIR="$bus_dir"
cd "$workspace"
echo "Copilot pane"
echo "machine: $machine_name"
echo "workspace: $workspace"
echo "bus: $bus_dir"
if [[ -n "$repo_dir" ]]; then
echo "helper: $repo_dir/scripts/ai-msg.sh"
fi
echo "login: start copilot and use /login if needed"
echo
copilot
status=$?
if [[ $status -ne 0 ]]; then
echo
echo "Copilot exited with status $status."
echo "If macOS reports 'SecItemCopyMatching failed -50', open copilot manually once and re-run /login."
fi
exit "$status"
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
tabs_file="${ZELLIJ_AI_FLEET_TABS_FILE:-}"
tabs_created_file="${ZELLIJ_AI_FLEET_CREATED_FILE:-}"
current_label="${ZELLIJ_AI_FLEET_CURRENT_LABEL:-}"
current_wrapper="${ZELLIJ_AI_FLEET_CURRENT_WRAPPER:-}"
if [[ -z "$current_wrapper" || ! -x "$current_wrapper" ]]; then
echo "Fleet bootstrap wrapper not found: $current_wrapper" >&2
exit 1
fi
if [[ -n "$current_label" ]]; then
zellij action rename-tab "$current_label"
fi
if [[ -n "$tabs_created_file" ]]; then
mkdir -p "$(dirname "$tabs_created_file")"
fi
if [[ -n "$tabs_file" && -f "$tabs_file" && -n "$tabs_created_file" && ! -f "$tabs_created_file" ]]; then
: >"$tabs_created_file"
while IFS=$'\t' read -r tab_label layout_file tab_cwd; do
if [[ -z "$tab_label" || -z "$layout_file" ]]; then
continue
fi
if [[ -n "$tab_cwd" ]]; then
zellij action new-tab -n "$tab_label" -l "$layout_file" --cwd "$tab_cwd"
else
zellij action new-tab -n "$tab_label" -l "$layout_file"
fi
done <"$tabs_file"
if [[ -n "$current_label" ]]; then
zellij action go-to-tab-name "$current_label"
fi
fi
exec "$current_wrapper"
+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
repo_source="$(cd "$script_dir/.." && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
usage() {
local script_name="${CONSTANT_SCRIPT_NAME:-$(basename "$0")}"
cat <<EOF
Usage: ${script_name} <check|install> [options]
Check or install the Constant fleet runtime on one or more machines.
Options:
--machine SPEC Machine definition or label to target
--repo-dir DIR Repository path on each machine
default: $(zellij_ai_default_repo_dir)
--codex-image IMAGE Deprecated, ignored
--yes Required for non-interactive install mode
-h, --help Show this help
EOF
}
warn_deprecated() {
printf '[fleet-install] %s\n' "$*" >&2
}
require_local_tools() {
local tool
for tool in bash ssh tar rsync; do
zellij_ai_require_command "$tool"
done
}
remote_home() {
ssh "$1" 'printf "%s\n" "$HOME"'
}
remote_repo_dir() {
local target="$1"
local repo_dir_input="$2"
local target_home
if zellij_ai_is_local_target "$target"; then
zellij_ai_expand_home_path "$repo_dir_input"
return 0
fi
target_home="$(remote_home "$target")"
zellij_ai_expand_home_path_for_home "$repo_dir_input" "$target_home"
}
sync_repo_to_remote() {
local target="$1"
local repo_dir_input="$2"
local target_repo_dir local_real target_real
target_repo_dir="$(remote_repo_dir "$target" "$repo_dir_input")"
if zellij_ai_is_local_target "$target"; then
mkdir -p "$target_repo_dir"
local_real="$repo_source"
target_real="$(cd "$target_repo_dir" && pwd -P)"
if [[ "$local_real" == "$target_real" ]]; then
return 0
fi
rsync -a --delete --exclude '.git/' "$repo_source/" "$target_repo_dir/"
return 0
fi
ssh "$target" "mkdir -p $(printf '%q' "$target_repo_dir")"
if ssh "$target" 'command -v rsync >/dev/null 2>&1'; then
rsync -a --delete --exclude '.git/' "$repo_source/" "${target}:${target_repo_dir}/"
else
(
cd "$repo_source"
tar --exclude '.git' -cf - .
) | ssh "$target" "tar -xf - -C $(printf '%q' "$target_repo_dir")"
fi
}
mode="${1:-}"
if [[ -z "$mode" ]]; then
usage >&2
exit 2
fi
case "$mode" in
-h|--help|help)
usage
exit 0
;;
esac
shift || true
machines=()
repo_dir="$(zellij_ai_default_repo_dir)"
yes=false
while [[ $# -gt 0 ]]; do
case "$1" in
--machine)
machines+=("$2")
shift 2
;;
--repo-dir)
repo_dir="$2"
shift 2
;;
--codex-image)
warn_deprecated "--codex-image is deprecated and ignored."
shift 2
;;
--yes)
yes=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
case "$mode" in
check|install)
;;
*)
echo "Unknown mode: $mode" >&2
usage >&2
exit 2
;;
esac
require_local_tools
if [[ ${#machines[@]} -eq 0 ]]; then
mapfile -t machines < <(zellij_ai_default_machine_specs)
fi
overall_status=0
for machine_spec in "${machines[@]}"; do
if ! resolved="$(zellij_ai_resolve_machine_spec "$machine_spec")"; then
echo "Unknown machine: $machine_spec" >&2
overall_status=1
continue
fi
IFS=$'\t' read -r label target <<<"$resolved"
printf '=== %s (%s) ===\n' "$label" "$target"
target_repo_dir="$(remote_repo_dir "$target" "$repo_dir")"
if [[ "$mode" == "install" ]]; then
sync_repo_to_remote "$target" "$repo_dir" || overall_status=1
fi
install_args=("$mode" --repo-dir "$repo_dir")
if $yes; then
install_args+=(--yes)
fi
if zellij_ai_is_local_target "$target"; then
ZELLIJ_AI_MACHINE_NAME="$label" "$script_dir/zellij-ai-machine-install.sh" "${install_args[@]}" || overall_status=1
continue
fi
if [[ "$mode" == "install" ]]; then
remote_script="${target_repo_dir}/scripts/zellij-ai-machine-install.sh"
ssh -tt "$target" env ZELLIJ_AI_MACHINE_NAME="$label" "$remote_script" "${install_args[@]}" || overall_status=1
continue
fi
if $yes; then
ssh "$target" env ZELLIJ_AI_MACHINE_NAME="$label" bash -s -- "${install_args[@]}" <"$script_dir/zellij-ai-machine-install.sh" || overall_status=1
else
ssh "$target" env ZELLIJ_AI_MACHINE_NAME="$label" bash -s -- "${install_args[@]}" <"$script_dir/zellij-ai-machine-install.sh" || overall_status=1
fi
done
exit "$overall_status"
+250
View File
@@ -0,0 +1,250 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
repo_dir="$(cd "$script_dir/.." && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
warn_deprecated() {
printf 'Warning: %s\n' "$*" >&2
}
usage() {
local script_name="${CONSTANT_SCRIPT_NAME:-$(basename "$0")}"
cat <<EOF
Usage: ${script_name} [options]
Open a local Zellij cockpit with one tab per machine.
Each tab attaches to the machine's own local Constant machine session.
Options:
--machine SPEC Machine definition. Repeat once per machine.
format: [TAB_NAME=]TARGET
default fleet: command-center, builder-a, builder-b, edge-a, lab-a
--session NAME Remote/local machine session name
default: $(zellij_ai_default_session)
--local-session NAME Cockpit session name on this machine
default: $(zellij_ai_default_local_session)
--repo-dir DIR Repository path on each machine
default: $(zellij_ai_default_repo_dir)
--workspace DIR Workspace path passed to each machine session
required for the fixed 5-machine fleet
--codex-image IMAGE Deprecated, ignored
--claude-config DIR Optional Claude config override passed to each machine
--remote-recreate Recreate each machine session before attaching
--recreate Recreate the local cockpit session before launching
-h, --help Show this help
EOF
}
machines=()
remote_session="$(zellij_ai_default_session)"
local_session="$(zellij_ai_default_local_session)"
remote_repo_dir="$(zellij_ai_default_repo_dir)"
workspace=""
codex_image=""
claude_config_dir=""
recreate=false
remote_recreate=false
while [[ $# -gt 0 ]]; do
case "$1" in
--machine)
machines+=("$2")
shift 2
;;
--session)
remote_session="$2"
shift 2
;;
--local-session)
local_session="$2"
shift 2
;;
--repo-dir)
remote_repo_dir="$2"
shift 2
;;
--workspace)
workspace="$2"
shift 2
;;
--codex-image)
warn_deprecated "--codex-image is deprecated and ignored; Codex now runs on the host."
codex_image="$2"
shift 2
;;
--claude-config)
claude_config_dir="$2"
shift 2
;;
--remote-recreate)
remote_recreate=true
shift
;;
--recreate)
recreate=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
if [[ ${#machines[@]} -eq 0 ]]; then
mapfile -t machines < <(zellij_ai_default_machine_specs)
fi
if [[ -z "$workspace" ]]; then
echo "--workspace is required for fleet mode." >&2
exit 2
fi
workspace="$(cd "$workspace" && pwd -P)"
for required in \
zellij \
ssh \
"$script_dir/zellij-ai-common.sh" \
"$script_dir/zellij-ai-fleet-bootstrap.sh" \
"$script_dir/zellij-ai-remote-tab.sh"
do
if [[ "$required" == */* ]]; then
if [[ ! -x "$required" ]]; then
echo "Helper script not executable: $required" >&2
exit 1
fi
else
zellij_ai_require_command "$required"
fi
done
state_dir="$(zellij_ai_fleet_state_dir "$local_session")"
tabs_file="$state_dir/tabs.tsv"
tabs_created_file="$state_dir/tabs-created"
first_layout_file="$state_dir/first-tab.kdl"
mkdir -p "$state_dir"
: >"$tabs_file"
rm -f "$tabs_created_file"
bootstrap_path="$(zellij_ai_kdl_escape "$script_dir/zellij-ai-fleet-bootstrap.sh")"
first_label=""
first_wrapper=""
seen_labels='|'
machine_index=0
for machine_spec in "${machines[@]}"; do
if ! parsed_spec="$(zellij_ai_parse_machine_spec "$machine_spec")"; then
echo "Invalid machine spec: $machine_spec" >&2
exit 2
fi
IFS=$'\t' read -r machine_label machine_target <<<"$parsed_spec"
if [[ "$seen_labels" == *"|${machine_label}|"* ]]; then
echo "Duplicate machine label: $machine_label" >&2
exit 2
fi
seen_labels+="${machine_label}|"
wrapper_path="$state_dir/machine-${machine_index}.sh"
layout_path="$state_dir/machine-${machine_index}.kdl"
wrapper_cmd=(
"$script_dir/zellij-ai-remote-tab.sh"
--label "$machine_label"
--target "$machine_target"
--session "$remote_session"
--repo-dir "$remote_repo_dir"
)
if [[ -n "$workspace" ]]; then
wrapper_cmd+=(--workspace "$workspace")
fi
if [[ -n "$codex_image" ]]; then
wrapper_cmd+=(--codex-image "$codex_image")
fi
if [[ -n "$claude_config_dir" ]]; then
wrapper_cmd+=(--claude-config "$claude_config_dir")
fi
if $remote_recreate; then
wrapper_cmd+=(--remote-recreate)
fi
printf -v wrapper_exec '%q ' "${wrapper_cmd[@]}"
wrapper_exec="${wrapper_exec% }"
cat >"$wrapper_path" <<EOF
#!/usr/bin/env bash
set -euo pipefail
exec $wrapper_exec
EOF
chmod +x "$wrapper_path"
wrapper_path_escaped="$(zellij_ai_kdl_escape "$wrapper_path")"
cat >"$layout_path" <<EOF
layout {
pane size=1 borderless=true {
plugin location="tab-bar"
}
pane command="$wrapper_path_escaped"
pane size=2 borderless=true {
plugin location="status-bar"
}
}
EOF
if [[ $machine_index -eq 0 ]]; then
first_label="$machine_label"
first_wrapper="$wrapper_path"
else
printf '%s\t%s\t%s\n' "$machine_label" "$layout_path" "$repo_dir" >>"$tabs_file"
fi
machine_index=$((machine_index + 1))
done
if [[ -z "$first_label" || -z "$first_wrapper" ]]; then
echo "No machine tabs were generated" >&2
exit 1
fi
cat >"$first_layout_file" <<EOF
layout {
pane size=1 borderless=true {
plugin location="tab-bar"
}
pane command="$bootstrap_path"
pane size=2 borderless=true {
plugin location="status-bar"
}
}
EOF
export ZELLIJ_AI_FLEET_TABS_FILE="$tabs_file"
export ZELLIJ_AI_FLEET_CREATED_FILE="$tabs_created_file"
export ZELLIJ_AI_FLEET_CURRENT_LABEL="$first_label"
export ZELLIJ_AI_FLEET_CURRENT_WRAPPER="$first_wrapper"
if $recreate && zellij_ai_session_exists "$local_session"; then
zellij kill-session "$local_session" >/dev/null 2>&1 || true
fi
if zellij_ai_session_exists "$local_session"; then
exec zellij attach "$local_session"
fi
exec zellij --new-session-with-layout "$first_layout_file" --session "$local_session"
+411
View File
@@ -0,0 +1,411 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
if [[ -f "$script_dir/zellij-ai-common.sh" ]]; then
source "$script_dir/zellij-ai-common.sh"
else
zellij_ai_default_repo_dir() {
printf '%s\n' '$HOME/constant'
}
zellij_ai_default_codex_home() {
printf '%s\n' '$HOME/.codex-profiles/codex'
}
zellij_ai_expand_home_path() {
case "$1" in
'$HOME'|'$HOME/'*)
printf '%s\n' "${HOME}${1#\$HOME}"
;;
"~")
printf '%s\n' "$HOME"
;;
"~/"*)
printf '%s\n' "$HOME/${1#~/}"
;;
*)
printf '%s\n' "$1"
;;
esac
}
zellij_ai_current_machine_name() {
if [[ -n "${ZELLIJ_AI_MACHINE_NAME:-}" ]]; then
printf '%s\n' "${ZELLIJ_AI_MACHINE_NAME}"
else
hostname -s 2>/dev/null || hostname || printf 'local'
fi
}
zellij_ai_agent_path() {
local path_value="$PATH"
if [[ -d "$HOME/.local/bin" ]]; then
path_value="$HOME/.local/bin:$path_value"
fi
if [[ -d "$HOME/.npm-global/bin" ]]; then
path_value="$HOME/.npm-global/bin:$path_value"
fi
printf '%s\n' "$path_value"
}
fi
usage() {
cat <<EOF
Usage: $(basename "$0") <check|install> [options]
Options:
--repo-dir DIR Repository path on this machine
default: $(zellij_ai_default_repo_dir)
--codex-image IMAGE Deprecated, ignored
--yes Required for non-interactive install mode
-h, --help Show this help
EOF
}
log() {
printf '[install] %s\n' "$*"
}
fail() {
printf '[install] %s\n' "$*" >&2
exit 1
}
confirm_install() {
if $yes; then
return 0
fi
if [[ ! -t 0 ]]; then
fail "Install mode requires --yes when stdin is not interactive."
fi
printf 'Proceed with installation changes on this machine? [y/N] '
read -r answer
[[ "$answer" == "y" || "$answer" == "Y" ]]
}
need_sudo() {
[[ "${EUID:-$(id -u)}" -ne 0 ]]
}
run_root() {
if ! need_sudo; then
"$@"
return 0
fi
if command -v sudo >/dev/null 2>&1; then
sudo "$@"
return 0
fi
fail "sudo is required to run: $*"
}
ensure_command() {
command -v "$1" >/dev/null 2>&1
}
warn_deprecated() {
printf '[install] %s\n' "$*" >&2
}
refresh_path() {
export PATH="$(zellij_ai_agent_path)"
if [[ -x /opt/homebrew/bin/brew ]]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
elif [[ -x /usr/local/bin/brew ]]; then
eval "$(/usr/local/bin/brew shellenv)"
fi
}
ensure_homebrew() {
if command -v brew >/dev/null 2>&1; then
return 0
fi
ensure_command curl || fail "curl is required to install Homebrew."
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
refresh_path
ensure_command brew || fail "Homebrew installation did not make brew available."
}
ensure_uv() {
if command -v uv >/dev/null 2>&1; then
return 0
fi
case "$os_name" in
Darwin)
ensure_homebrew
brew install uv
;;
Linux)
ensure_command curl || fail "curl is required to install uv."
curl -LsSf https://astral.sh/uv/install.sh | sh
;;
*)
fail "Unsupported OS for uv installation: $os_name"
;;
esac
refresh_path
ensure_command uv || fail "uv installation did not make uv available."
}
ensure_npm_global() {
local package_name="$1"
local binary_name="$2"
if command -v "$binary_name" >/dev/null 2>&1; then
return 0
fi
mkdir -p "$HOME/.npm-global"
export PATH="$(zellij_ai_agent_path)"
if npm install -g --prefix "$HOME/.npm-global" "$package_name"; then
return 0
fi
fail "Failed to install ${package_name} with npm."
}
ensure_uv_tool() {
local package_name="$1"
local binary_name="$2"
if command -v "$binary_name" >/dev/null 2>&1; then
return 0
fi
ensure_uv
uv tool install "$package_name"
refresh_path
ensure_command "$binary_name" || fail "Failed to install ${package_name} with uv."
}
os_name="$(uname -s)"
linux_id=""
repo_dir="$(zellij_ai_expand_home_path "$(zellij_ai_default_repo_dir)")"
codex_home="$(zellij_ai_expand_home_path "$(zellij_ai_default_codex_home)")"
yes=false
refresh_path
if [[ -f /etc/os-release ]]; then
# shellcheck disable=SC1091
source /etc/os-release
linux_id="${ID:-}"
fi
if [[ $# -lt 1 ]]; then
usage >&2
exit 2
fi
mode="$1"
case "$mode" in
-h|--help|help)
usage
exit 0
;;
esac
shift
while [[ $# -gt 0 ]]; do
case "$1" in
--repo-dir)
repo_dir="$(zellij_ai_expand_home_path "$2")"
shift 2
;;
--codex-image)
warn_deprecated "--codex-image is deprecated and ignored."
shift 2
;;
--yes)
yes=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
print_status() {
local key="$1"
local value="$2"
printf '%s=%s\n' "$key" "$value"
}
run_check() {
local ok=true
local repo_state="missing"
local codex_home_state="missing"
local triple_help_state="missing"
local binary
refresh_path
print_status machine "$(zellij_ai_current_machine_name)"
print_status os "$os_name"
[[ -n "$linux_id" ]] && print_status linux_id "$linux_id"
print_status repo_dir "$repo_dir"
print_status codex_home "$codex_home"
for binary in bash git zellij node npm uv claude codex copilot vibe; do
if command -v "$binary" >/dev/null 2>&1; then
print_status "$binary" "$(command -v "$binary")"
else
print_status "$binary" "missing"
ok=false
fi
done
if [[ -x "$repo_dir/scripts/constant-machine.sh" || -x "$repo_dir/scripts/zellij-ai-triple.sh" ]]; then
repo_state="present"
else
ok=false
fi
print_status repo "$repo_state"
if [[ -d "$codex_home" ]]; then
codex_home_state="present"
else
ok=false
fi
print_status codex_profile "$codex_home_state"
if [[ -x "$repo_dir/scripts/constant-machine.sh" ]] && "$repo_dir/scripts/constant-machine.sh" --help >/dev/null 2>&1; then
triple_help_state="ok"
elif [[ -x "$repo_dir/scripts/zellij-ai-triple.sh" ]] && "$repo_dir/scripts/zellij-ai-triple.sh" --help >/dev/null 2>&1; then
triple_help_state="ok"
else
ok=false
fi
print_status triple_help "$triple_help_state"
$ok
}
install_darwin() {
ensure_homebrew
brew install git zellij node uv
}
install_linux_ubuntu() {
run_root apt-get update
run_root apt-get install -y git curl
if ! command -v node >/dev/null 2>&1; then
run_root apt-get install -y nodejs
fi
if ! command -v npm >/dev/null 2>&1; then
run_root apt-get install -y npm
fi
if ! command -v zellij >/dev/null 2>&1; then
if command -v snap >/dev/null 2>&1; then
run_root snap install zellij --classic
else
run_root apt-get install -y zellij
fi
fi
}
install_linux_photon() {
local packages=()
if ! command -v git >/dev/null 2>&1; then
packages+=(git)
fi
if ! command -v curl >/dev/null 2>&1; then
packages+=(curl)
fi
if ! command -v node >/dev/null 2>&1; then
packages+=(nodejs)
fi
if ! command -v npm >/dev/null 2>&1; then
packages+=(npm)
fi
if [[ ${#packages[@]} -gt 0 ]]; then
run_root tdnf install -y "${packages[@]}"
fi
if ! command -v zellij >/dev/null 2>&1; then
fail "zellij is missing on Photon OS and no native install path is scripted here."
fi
}
run_install() {
confirm_install || fail "Installation cancelled."
mkdir -p "$repo_dir"
case "$os_name" in
Darwin)
install_darwin
;;
Linux)
case "$linux_id" in
ubuntu|debian)
install_linux_ubuntu
;;
photon)
install_linux_photon
;;
*)
fail "Unsupported Linux distribution: ${linux_id:-unknown}"
;;
esac
;;
*)
fail "Unsupported OS: $os_name"
;;
esac
refresh_path
mkdir -p "$codex_home"
ensure_npm_global "@anthropic-ai/claude-code" claude
ensure_npm_global "@openai/codex" codex
ensure_npm_global "@github/copilot" copilot
ensure_uv_tool "mistral-vibe" vibe
run_check
}
case "$mode" in
check)
run_check
;;
install)
run_install
;;
*)
echo "Unknown mode: $mode" >&2
usage >&2
exit 2
;;
esac
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
warn_deprecated() {
printf 'Warning: %s\n' "$*" >&2
}
usage() {
cat <<'EOF'
Usage: zellij-ai-remote-tab.sh [options]
Open or attach the local AI Zellij session for one machine.
Options:
--label NAME Display label for logs and tab naming
--target TARGET SSH target, or "local" for the current machine
--session NAME Remote/local Zellij session name
default: constant
--repo-dir DIR Repository path on the target machine
default: $HOME/constant
--workspace DIR Workspace path passed to constant-machine.sh
--codex-image IMAGE Deprecated, ignored
--claude-config DIR Optional Claude config override
--remote-recreate Recreate the target machine session before attaching
-h, --help Show this help
EOF
}
label=""
target=""
session="$(zellij_ai_default_session)"
repo_dir="$(zellij_ai_default_repo_dir)"
workspace=""
codex_image=""
claude_config_dir=""
remote_recreate=false
while [[ $# -gt 0 ]]; do
case "$1" in
--label)
label="$2"
shift 2
;;
--target)
target="$2"
shift 2
;;
--session)
session="$2"
shift 2
;;
--repo-dir)
repo_dir="$2"
shift 2
;;
--workspace)
workspace="$2"
shift 2
;;
--codex-image)
warn_deprecated "--codex-image is deprecated and ignored; Codex now runs on the host."
codex_image="$2"
shift 2
;;
--claude-config)
claude_config_dir="$2"
shift 2
;;
--remote-recreate)
remote_recreate=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
if [[ -z "$target" ]]; then
echo "--target is required" >&2
usage >&2
exit 2
fi
if [[ -z "$label" ]]; then
label="$target"
fi
if zellij_ai_is_local_target "$target"; then
local_repo_dir="$(zellij_ai_expand_home_path "$repo_dir")"
local_workspace="$(zellij_ai_expand_home_path "$workspace")"
local_claude_config_dir="$(zellij_ai_expand_home_path "$claude_config_dir")"
local_launcher="$local_repo_dir/scripts/constant-machine.sh"
if [[ ! -x "$local_launcher" ]]; then
local_launcher="$local_repo_dir/scripts/zellij-ai-triple.sh"
fi
if [[ ! -x "$local_launcher" ]]; then
echo "Local launcher not found for $label: $local_launcher" >&2
exit 1
fi
cmd=("$local_launcher" --session "$session")
if [[ -n "$workspace" ]]; then
cmd+=(--workspace "$local_workspace")
fi
if [[ -n "$codex_image" ]]; then
cmd+=(--codex-image "$codex_image")
fi
if [[ -n "$claude_config_dir" ]]; then
cmd+=(--claude-config "$local_claude_config_dir")
fi
if $remote_recreate; then
cmd+=(--recreate)
fi
exec env ZELLIJ_AI_MACHINE_NAME="$label" "${cmd[@]}"
fi
printf -v session_q '%q' "$session"
printf -v repo_dir_q '%q' "$repo_dir"
printf -v workspace_q '%q' "$workspace"
printf -v codex_image_q '%q' "$codex_image"
printf -v claude_config_dir_q '%q' "$claude_config_dir"
printf -v label_q '%q' "$label"
remote_recreate_literal=false
if $remote_recreate; then
remote_recreate_literal=true
fi
read -r -d '' remote_shell <<EOF || true
set -euo pipefail
expand_home_path() {
case "\$1" in
'\$HOME'|'\$HOME/'*)
printf '%s\n' "\${HOME}\${1#\\\$HOME}"
;;
"~")
printf '%s\n' "\$HOME"
;;
"~/"*)
printf '%s\n' "\$HOME/\${1#~/}"
;;
*)
printf '%s\n' "\$1"
;;
esac
}
session=$session_q
repo_dir_input=$repo_dir_q
workspace_input=$workspace_q
codex_image_input=$codex_image_q
claude_config_dir_input=$claude_config_dir_q
remote_recreate=$remote_recreate_literal
repo_dir="\$(expand_home_path "\$repo_dir_input")"
workspace="\$(expand_home_path "\$workspace_input")"
claude_config_dir="\$(expand_home_path "\$claude_config_dir_input")"
launcher="\$repo_dir/scripts/constant-machine.sh"
if [[ ! -x "\$launcher" ]]; then
launcher="\$repo_dir/scripts/zellij-ai-triple.sh"
fi
if [[ ! -x "\$launcher" ]]; then
echo "Remote launcher not found: \$launcher" >&2
exit 1
fi
cmd=("\$launcher" --session "\$session")
if [[ -n "\$workspace_input" ]]; then
cmd+=(--workspace "\$workspace")
fi
if [[ -n "\$codex_image_input" ]]; then
cmd+=(--codex-image "\$codex_image_input")
fi
if [[ -n "\$claude_config_dir_input" ]]; then
cmd+=(--claude-config "\$claude_config_dir")
fi
if \$remote_recreate; then
cmd+=(--recreate)
fi
export ZELLIJ_AI_MACHINE_NAME=$label_q
export ZELLIJ_AI_FORCE_OSC52=true
exec "\${cmd[@]}"
EOF
exec ssh -tt "$target" "bash -lc $(printf '%q' "$remote_shell")"
+110 -60
View File
@@ -1,61 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
repo_dir="$(cd "$script_dir/.." && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
usage() {
local script_name="${CONSTANT_SCRIPT_NAME:-$(basename "$0")}"
cat <<EOF
Usage: $(basename "$0") [options]
Usage: ${script_name} [options]
Start a 3-pane Zellij session:
Start a 4-pane Constant machine session:
- left pane: Claude on the host
- top-right pane: Codex in Docker with profile 1
- bottom-right pane: Codex in Docker with profile 2
- top-right pane: Codex on the host
- middle-right pane: Copilot CLI on the host
- bottom-right pane: Mistral Vibe on the host
Options:
--workspace DIR Workspace mounted in both Codex containers and used by Claude
--workspace DIR Workspace used by all panes
default: current directory
--session NAME Zellij session name
default: ai-triple
--codex-image IMAGE Docker image used for Codex panes
default: codercom/code-server:latest
--codex1-home DIR Persistent CODEX_HOME for the first Codex pane
default: \$HOME/.codex-profiles/codex-1
--codex2-home DIR Persistent CODEX_HOME for the second Codex pane
default: \$HOME/.codex-profiles/codex-2
--codex1-label LABEL Display label for the first Codex pane
default: codex-1
--codex2-label LABEL Display label for the second Codex pane
default: codex-2
--session NAME Session name
default: $(zellij_ai_default_session)
--codex-home DIR Persistent CODEX_HOME for the Codex pane
default: $(zellij_ai_default_codex_home)
--codex-label LABEL Display label for the Codex pane
default: codex
--copilot-label LABEL Display label for the Copilot pane
default: copilot
--vibe-label LABEL Display label for the Vibe pane
default: vibe
--claude-config DIR Optional CLAUDE_CONFIG_DIR override for the Claude pane
--zellij-config-dir DIR
Optional isolated Zellij config dir to use for session creation
Optional isolated Zellij config dir for session creation
default: a fresh temporary config dir per new session
--recreate Kill the existing session before recreating it
--codex-image IMAGE Deprecated, ignored
--codex1-home DIR Deprecated alias for --codex-home
--codex2-home DIR Deprecated, ignored
--codex1-label LABEL Deprecated alias for --codex-label
--codex2-label LABEL Deprecated, ignored
--recreate Recreate the session and purge stale Zellij state first
-h, --help Show this help
EOF
}
strip_ansi() {
sed -E 's/\x1b\[[0-9;]*m//g'
}
session_exists() {
zellij list-sessions 2>/dev/null | strip_ansi | awk '{print $1}' | grep -Fxq "$1"
}
kdl_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
warn_deprecated() {
printf 'Warning: %s\n' "$*" >&2
}
workspace="$PWD"
session="ai-triple"
codex_image="codercom/code-server:latest"
codex1_home="$HOME/.codex-profiles/codex-1"
codex2_home="$HOME/.codex-profiles/codex-2"
codex1_label="codex-1"
codex2_label="codex-2"
session="$(zellij_ai_default_session)"
codex_home="$(zellij_ai_default_codex_home)"
codex_label="codex"
copilot_label="copilot"
vibe_label="vibe"
claude_config_dir=""
zellij_config_dir=""
recreate=false
@@ -70,24 +73,42 @@ while [[ $# -gt 0 ]]; do
session="$2"
shift 2
;;
--codex-home)
codex_home="$2"
shift 2
;;
--codex-label)
codex_label="$2"
shift 2
;;
--copilot-label)
copilot_label="$2"
shift 2
;;
--vibe-label)
vibe_label="$2"
shift 2
;;
--codex-image)
codex_image="$2"
warn_deprecated "--codex-image is deprecated and ignored; Codex now runs on the host."
shift 2
;;
--codex1-home)
codex1_home="$2"
warn_deprecated "--codex1-home is deprecated; use --codex-home."
codex_home="$2"
shift 2
;;
--codex2-home)
codex2_home="$2"
warn_deprecated "--codex2-home is deprecated and ignored."
shift 2
;;
--codex1-label)
codex1_label="$2"
warn_deprecated "--codex1-label is deprecated; use --codex-label."
codex_label="$2"
shift 2
;;
--codex2-label)
codex2_label="$2"
warn_deprecated "--codex2-label is deprecated and ignored."
shift 2
;;
--claude-config)
@@ -115,11 +136,14 @@ while [[ $# -gt 0 ]]; do
done
workspace="$(cd "$workspace" && pwd -P)"
codex1_home="$(mkdir -p "$codex1_home" && cd "$codex1_home" && pwd -P)"
codex2_home="$(mkdir -p "$codex2_home" && cd "$codex2_home" && pwd -P)"
codex_home="$(mkdir -p "$(zellij_ai_expand_home_path "$codex_home")" && cd "$(zellij_ai_expand_home_path "$codex_home")" && pwd -P)"
bootstrap_file="$HOME/.cache/zellij-ai/${session}.bootstrapped"
layout_file="/tmp/zellij-ai-layout-${session}.kdl"
machine_name="$(zellij_ai_current_machine_name)"
state_dir="$(zellij_ai_session_state_dir "$session")"
bus_dir="$state_dir/bus"
bootstrap_file="$state_dir/bootstrapped"
layout_file="$state_dir/layout.kdl"
clipboard_config_file="$state_dir/zellij.config.kdl"
if [[ ! -d "$workspace" ]]; then
echo "Workspace not found: $workspace" >&2
@@ -129,7 +153,10 @@ fi
for required in \
"$script_dir/zellij-ai-claude-pane.sh" \
"$script_dir/zellij-ai-codex-pane.sh" \
"$script_dir/zellij-ai-bootstrap.sh"
"$script_dir/zellij-ai-copilot-pane.sh" \
"$script_dir/zellij-ai-vibe-pane.sh" \
"$script_dir/zellij-ai-bootstrap.sh" \
"$script_dir/zellij-ai-common.sh"
do
if [[ ! -x "$required" ]]; then
echo "Helper script not executable: $required" >&2
@@ -137,36 +164,58 @@ do
fi
done
mkdir -p "$bus_dir/messages"
if [[ "${ZELLIJ_AI_FORCE_OSC52:-false}" == "true" || "${ZELLIJ_AI_FORCE_OSC52:-0}" == "1" ]]; then
cat >"$clipboard_config_file" <<'EOF'
copy_clipboard "system"
EOF
elif [[ "$(uname -s)" == "Darwin" && -x /usr/bin/pbcopy ]]; then
cat >"$clipboard_config_file" <<'EOF'
copy_command "pbcopy"
copy_clipboard "system"
EOF
else
cat >"$clipboard_config_file" <<'EOF'
copy_clipboard "system"
EOF
fi
export ZELLIJ_AI_WORKSPACE="$workspace"
export ZELLIJ_AI_CODEX_IMAGE="$codex_image"
export ZELLIJ_AI_CODEX1_HOME="$codex1_home"
export ZELLIJ_AI_CODEX2_HOME="$codex2_home"
export ZELLIJ_AI_CODEX1_LABEL="$codex1_label"
export ZELLIJ_AI_CODEX2_LABEL="$codex2_label"
export ZELLIJ_AI_CODEX_HOME="$codex_home"
export ZELLIJ_AI_CODEX_LABEL="$codex_label"
export ZELLIJ_AI_COPILOT_LABEL="$copilot_label"
export ZELLIJ_AI_VIBE_LABEL="$vibe_label"
export ZELLIJ_AI_MAIN_CODEX_CONFIG="$HOME/.codex/config.toml"
export ZELLIJ_AI_BOOTSTRAP_FILE="$bootstrap_file"
export ZELLIJ_AI_REPO_DIR="$repo_dir"
export ZELLIJ_AI_SESSION="$session"
export ZELLIJ_AI_STATE_DIR="$state_dir"
export ZELLIJ_AI_BUS_DIR="$bus_dir"
export ZELLIJ_AI_MACHINE_NAME="$machine_name"
if [[ -n "$claude_config_dir" ]]; then
export ZELLIJ_AI_CLAUDE_CONFIG_DIR="$claude_config_dir"
fi
if $recreate && session_exists "$session"; then
zellij_attach_cmd=(zellij --config "$clipboard_config_file")
if $recreate && zellij_ai_session_exists "$session"; then
zellij delete-session -f "$session" >/dev/null 2>&1 || true
fi
if session_exists "$session"; then
exec zellij attach "$session"
if zellij_ai_session_exists "$session"; then
exec "${zellij_attach_cmd[@]}" attach "$session"
fi
zellij delete-session "$session" >/dev/null 2>&1 || true
mkdir -p "$(dirname "$bootstrap_file")"
mkdir -p "$state_dir"
rm -f "$bootstrap_file"
bootstrap_path="$(kdl_escape "$script_dir/zellij-ai-bootstrap.sh")"
bootstrap_path="$(zellij_ai_kdl_escape "$script_dir/zellij-ai-bootstrap.sh")"
cat >"$layout_file" <<EOF
layout {
tab name="AI Triple" {
tab name="Constant" {
pane size=1 borderless=true {
plugin location="tab-bar"
}
@@ -179,9 +228,10 @@ layout {
EOF
if [[ -z "$zellij_config_dir" ]]; then
zellij_config_dir="$(mktemp -d "${TMPDIR:-/tmp}/zellij-ai-triple-${session}.XXXXXX")"
zellij_config_dir="$(mktemp -d "${TMPDIR:-/tmp}/constant-zellij-${session}.XXXXXX")"
else
zellij_config_dir="$(zellij_ai_expand_home_path "$zellij_config_dir")"
mkdir -p "$zellij_config_dir"
fi
exec zellij --config-dir "$zellij_config_dir" --new-session-with-layout "$layout_file" --session "$session"
exec zellij --config "$clipboard_config_file" --config-dir "$zellij_config_dir" --new-session-with-layout "$layout_file" --session "$session"
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}"
machine_name="${ZELLIJ_AI_MACHINE_NAME:-unknown}"
session_name="${ZELLIJ_AI_SESSION:-$(zellij_ai_default_session)}"
repo_dir="${ZELLIJ_AI_REPO_DIR:-$(cd "$script_dir/.." && pwd -P)}"
bus_dir="${ZELLIJ_AI_BUS_DIR:-$(zellij_ai_session_state_dir "$session_name")/bus}"
label="${ZELLIJ_AI_VIBE_LABEL:-vibe}"
if [[ ! -d "$workspace" ]]; then
echo "Workspace not found: $workspace" >&2
exit 1
fi
export PATH="$(zellij_ai_agent_path)"
if [[ -n "$repo_dir" && -d "$repo_dir/scripts" ]]; then
export PATH="$repo_dir/scripts:$PATH"
fi
if ! command -v vibe >/dev/null 2>&1; then
echo "Mistral Vibe CLI not found on the host." >&2
echo "Install it with: uv tool install mistral-vibe" >&2
exit 1
fi
mkdir -p "$bus_dir/messages"
export ZELLIJ_AI_ROLE="$label"
export ZELLIJ_AI_MACHINE_NAME="$machine_name"
export ZELLIJ_AI_SESSION="$session_name"
export ZELLIJ_AI_BUS_DIR="$bus_dir"
cd "$workspace"
echo "Vibe pane"
echo "machine: $machine_name"
echo "workspace: $workspace"
echo "bus: $bus_dir"
if [[ -n "$repo_dir" ]]; then
echo "helper: $repo_dir/scripts/ai-msg.sh"
fi
echo
exec vibe