refactor: rename box/boxes to bottle/bottles in config schema and code

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 23:02:34 -04:00
parent c45f384fb8
commit 74a2c7a32a
2 changed files with 103 additions and 103 deletions
+50 -50
View File
@@ -96,8 +96,8 @@ cmd_info() {
prompt_len="${#prompt_content}" prompt_len="${#prompt_content}"
prompt_first_line="$(printf '%s' "$prompt_content" | awk 'NR==1{print; exit}')" prompt_first_line="$(printf '%s' "$prompt_content" | awk 'NR==1{print; exit}')"
local box_name local bottle_name
box_name="$(manifest_agent_box "$MANIFEST_FILE" "$NAME")" bottle_name="$(manifest_agent_bottle "$MANIFEST_FILE" "$NAME")"
local ssh_entries=() _se local ssh_entries=() _se
while IFS= read -r _se; do while IFS= read -r _se; do
@@ -110,8 +110,8 @@ cmd_info() {
info "env (names only): ${env_names:-(none)}" info "env (names only): ${env_names:-(none)}"
info "skills : ${skill_names[*]:-(none)}" info "skills : ${skill_names[*]:-(none)}"
info "prompt : ${prompt_len} chars; first line: ${prompt_first_line:-(empty)}" info "prompt : ${prompt_len} chars; first line: ${prompt_first_line:-(empty)}"
if [ -n "$box_name" ]; then if [ -n "$bottle_name" ]; then
info "box : ${box_name}" info "bottle : ${bottle_name}"
if [ "${#ssh_entries[@]}" -gt 0 ]; then if [ "${#ssh_entries[@]}" -gt 0 ]; then
local _n _h _u _p _k _khk local _n _h _u _p _k _khk
for _se in "${ssh_entries[@]}"; do for _se in "${ssh_entries[@]}"; do
@@ -128,7 +128,7 @@ cmd_info() {
info " ssh hosts : (none)" info " ssh hosts : (none)"
fi fi
else else
info "box : (none)" info "bottle : (none)"
fi fi
printf '\n' printf '\n'
} }
@@ -378,16 +378,16 @@ cmd_start() {
skills_validate_all "${SKILL_NAMES[@]}" skills_validate_all "${SKILL_NAMES[@]}"
fi fi
# Resolve the box referenced by this agent and validate it exists. # Resolve the bottle referenced by this agent and validate it exists.
# A box is required — agents without one are rejected before launch. # A bottle is required — agents without one are rejected before launch.
local BOX_NAME local BOTTLE_NAME
BOX_NAME="$(manifest_agent_box "$MANIFEST_FILE" "$NAME")" BOTTLE_NAME="$(manifest_agent_bottle "$MANIFEST_FILE" "$NAME")"
if [ -z "$BOX_NAME" ]; then if [ -z "$BOTTLE_NAME" ]; then
die "agent '${NAME}' has no 'box' field. Add a box association to this agent in claude-bottle.json." die "agent '${NAME}' has no 'bottle' field. Add a bottle association to this agent in claude-bottle.json."
fi fi
manifest_require_box "$MANIFEST_FILE" "$BOX_NAME" manifest_require_bottle "$MANIFEST_FILE" "$BOTTLE_NAME"
# SSH entries come from the agent's box (empty if no box set). # SSH entries come from the agent's bottle (empty if no bottle set).
local SSH_ENTRIES=() local SSH_ENTRIES=()
local _se local _se
while IFS= read -r _se; do while IFS= read -r _se; do
@@ -457,8 +457,8 @@ cmd_start() {
else else
info "skills : (none)" info "skills : (none)"
fi fi
if [ -n "$BOX_NAME" ]; then if [ -n "$BOTTLE_NAME" ]; then
info "box : ${BOX_NAME}" info "bottle : ${BOTTLE_NAME}"
if [ "${#SSH_ENTRIES[@]}" -gt 0 ]; then if [ "${#SSH_ENTRIES[@]}" -gt 0 ]; then
local _ssh_names="" _se local _ssh_names="" _se
for _se in "${SSH_ENTRIES[@]}"; do for _se in "${SSH_ENTRIES[@]}"; do
@@ -471,7 +471,7 @@ cmd_start() {
info " ssh hosts : (none)" info " ssh hosts : (none)"
fi fi
else else
info "box : (none)" info "bottle : (none)"
fi fi
info "prompt : ${PROMPT_LEN} chars; first line: ${PROMPT_FIRST_LINE:-(empty)}" info "prompt : ${PROMPT_LEN} chars; first line: ${PROMPT_FIRST_LINE:-(empty)}"
printf '\n' >&2 printf '\n' >&2
@@ -708,34 +708,34 @@ cmd_init() {
fi fi
done done
# --- Box association --- # --- Bottle association ---
printf '\n' >&2 printf '\n' >&2
printf 'Associate this agent with a box? [y/N] ' >&2 printf 'Associate this agent with a bottle? [y/N] ' >&2
local _box_yn="" local _bottle_yn=""
IFS= read -r _box_yn </dev/tty IFS= read -r _bottle_yn </dev/tty
local BOX_NAME="" local BOTTLE_NAME=""
local BOX_SSH_JSON='[]' local BOTTLE_SSH_JSON='[]'
local BOX_ENV_JSON='{}' local BOTTLE_ENV_JSON='{}'
local _box_exists=0 local _bottle_exists=0
case "$_box_yn" in case "$_bottle_yn" in
y|Y|yes|YES) y|Y|yes|YES)
while [ -z "$BOX_NAME" ]; do while [ -z "$BOTTLE_NAME" ]; do
printf ' Box name: ' >&2 printf ' Bottle name: ' >&2
IFS= read -r BOX_NAME </dev/tty IFS= read -r BOTTLE_NAME </dev/tty
BOX_NAME="$(printf '%s' "$BOX_NAME" | tr ' ' '-')" BOTTLE_NAME="$(printf '%s' "$BOTTLE_NAME" | tr ' ' '-')"
if [ -z "$BOX_NAME" ]; then if [ -z "$BOTTLE_NAME" ]; then
warn "box name cannot be empty" warn "bottle name cannot be empty"
fi fi
done done
# Check whether the box already exists in the target file. # Check whether the bottle already exists in the target file.
if [ -f "$TARGET_FILE" ] && jq -e --arg b "$BOX_NAME" '.boxes | has($b)' "$TARGET_FILE" >/dev/null 2>&1; then if [ -f "$TARGET_FILE" ] && jq -e --arg b "$BOTTLE_NAME" '.bottles | has($b)' "$TARGET_FILE" >/dev/null 2>&1; then
_box_exists=1 _bottle_exists=1
info "Box '${BOX_NAME}' already exists in ${TARGET_FILE}; agent will reference it." info "Bottle '${BOTTLE_NAME}' already exists in ${TARGET_FILE}; agent will reference it."
else else
info "Creating new box '${BOX_NAME}'." info "Creating new bottle '${BOTTLE_NAME}'."
# --- Env vars (stored on the box) --- # --- Env vars (stored on the bottle) ---
printf '\n' >&2 printf '\n' >&2
info "Env vars — enter each var name then its mode. Press Enter with no name to finish." info "Env vars — enter each var name then its mode. Press Enter with no name to finish."
info " Modes: secret (prompt at runtime) | interpolated (read from host env) | literal (hardcoded value)" info " Modes: secret (prompt at runtime) | interpolated (read from host env) | literal (hardcoded value)"
@@ -779,10 +779,10 @@ cmd_init() {
;; ;;
esac esac
BOX_ENV_JSON="$(printf '%s' "$BOX_ENV_JSON" | jq --arg k "$_vname" --arg v "$_vval" '.[$k] = $v')" BOTTLE_ENV_JSON="$(printf '%s' "$BOTTLE_ENV_JSON" | jq --arg k "$_vname" --arg v "$_vval" '.[$k] = $v')"
done done
printf ' Add SSH host entries to this box? [y/N] ' >&2 printf ' Add SSH host entries to this bottle? [y/N] ' >&2
local _ssh_yn="" local _ssh_yn=""
IFS= read -r _ssh_yn </dev/tty IFS= read -r _ssh_yn </dev/tty
case "$_ssh_yn" in case "$_ssh_yn" in
@@ -829,7 +829,7 @@ cmd_init() {
if [ -n "$_skhk" ]; then if [ -n "$_skhk" ]; then
_entry="$(printf '%s' "$_entry" | jq --arg khk "$_skhk" '. + {KnownHostKey: $khk}')" _entry="$(printf '%s' "$_entry" | jq --arg khk "$_skhk" '. + {KnownHostKey: $khk}')"
fi fi
BOX_SSH_JSON="$(printf '%s' "$BOX_SSH_JSON" | jq --argjson e "$_entry" '. + [$e]')" BOTTLE_SSH_JSON="$(printf '%s' "$BOTTLE_SSH_JSON" | jq --argjson e "$_entry" '. + [$e]')"
done done
;; ;;
esac esac
@@ -844,21 +844,21 @@ cmd_init() {
--arg prompt "$PROMPT_CONTENT" \ --arg prompt "$PROMPT_CONTENT" \
'{skills: $skills, prompt: $prompt}')" '{skills: $skills, prompt: $prompt}')"
if [ -n "$BOX_NAME" ]; then if [ -n "$BOTTLE_NAME" ]; then
AGENT_JSON="$(printf '%s' "$AGENT_JSON" | jq --arg b "$BOX_NAME" '. + {box: $b}')" AGENT_JSON="$(printf '%s' "$AGENT_JSON" | jq --arg b "$BOTTLE_NAME" '. + {bottle: $b}')"
fi fi
# Build the new entry. When creating a brand-new box, include it so the # Build the new entry. When creating a brand-new bottle, include it so the
# merge below writes both boxes and agents in one step. env lives on the box. # merge below writes both bottles and agents in one step. env lives on the bottle.
local NEW_ENTRY local NEW_ENTRY
if [ -n "$BOX_NAME" ] && [ "$_box_exists" = "0" ]; then if [ -n "$BOTTLE_NAME" ] && [ "$_bottle_exists" = "0" ]; then
NEW_ENTRY="$(jq -n \ NEW_ENTRY="$(jq -n \
--arg box_name "$BOX_NAME" \ --arg bottle_name "$BOTTLE_NAME" \
--argjson box_env "$BOX_ENV_JSON" \ --argjson bottle_env "$BOTTLE_ENV_JSON" \
--argjson box_ssh "$BOX_SSH_JSON" \ --argjson bottle_ssh "$BOTTLE_SSH_JSON" \
--arg agent_name "$AGENT_NAME" \ --arg agent_name "$AGENT_NAME" \
--argjson agent "$AGENT_JSON" \ --argjson agent "$AGENT_JSON" \
'{"boxes": {($box_name): {env: $box_env, ssh: $box_ssh}}, "agents": {($agent_name): $agent}}')" '{"bottles": {($bottle_name): {env: $bottle_env, ssh: $bottle_ssh}}, "agents": {($agent_name): $agent}}')"
else else
NEW_ENTRY="$(jq -n \ NEW_ENTRY="$(jq -n \
--arg name "$AGENT_NAME" \ --arg name "$AGENT_NAME" \
@@ -878,7 +878,7 @@ cmd_init() {
die "${TARGET_FILE} exists but is not valid JSON; fix or remove it first" die "${TARGET_FILE} exists but is not valid JSON; fix or remove it first"
fi fi
if ! printf '%s' "$NEW_ENTRY" | jq -s '{ if ! printf '%s' "$NEW_ENTRY" | jq -s '{
"boxes": ((.[0].boxes // {}) * (.[1].boxes // {})), "bottles": ((.[0].bottles // {}) * (.[1].bottles // {})),
"agents": ((.[0].agents // {}) * (.[1].agents // {})) "agents": ((.[0].agents // {}) * (.[1].agents // {}))
}' "$TARGET_FILE" - > "$TMP_FILE"; then }' "$TARGET_FILE" - > "$TMP_FILE"; then
rm -f "$TMP_FILE" rm -f "$TMP_FILE"
+53 -53
View File
@@ -5,8 +5,8 @@
# The manifest schema is documented in CLAUDE.md "Intended design". In # The manifest schema is documented in CLAUDE.md "Intended design". In
# short: # short:
# { # {
# "boxes": { # "bottles": {
# "<box-name>": { # "<bottle-name>": {
# "env": { "<NAME>": <env-entry>, ... }, # "env": { "<NAME>": <env-entry>, ... },
# "ssh": [ <ssh-entry>, ... ] # "ssh": [ <ssh-entry>, ... ]
# }, # },
@@ -16,14 +16,14 @@
# "<agent-name>": { # "<agent-name>": {
# "skills": [ "<skill-name>", ... ], # "skills": [ "<skill-name>", ... ],
# "prompt": "<string>", # "prompt": "<string>",
# "box": "<box-name>" # "bottle": "<bottle-name>"
# }, # },
# ... # ...
# } # }
# } # }
# #
# A box groups shared infrastructure (SSH keys, known hosts) that multiple # A bottle groups shared infrastructure (SSH keys, known hosts) that multiple
# agents can reference by name. The "box" field is required on every agent; # agents can reference by name. The "bottle" field is required on every agent;
# cli.sh start rejects agents that omit it. # cli.sh start rejects agents that omit it.
# #
# An <env-entry> is a JSON string. Mode is selected by sentinel prefix: # An <env-entry> is a JSON string. Mode is selected by sentinel prefix:
@@ -96,9 +96,9 @@ manifest_resolve() {
elif [ "$has_cwd" = "0" ] && [ "$has_home" = "1" ]; then elif [ "$has_cwd" = "0" ] && [ "$has_home" = "1" ]; then
cat "$home_file" cat "$home_file"
else else
# Merge: home is the base, cwd overrides on name conflict for both boxes and agents. # Merge: home is the base, cwd overrides on name conflict for both bottles and agents.
jq -s '{ jq -s '{
"boxes": ((.[0].boxes // {}) * (.[1].boxes // {})), "bottles": ((.[0].bottles // {}) * (.[1].bottles // {})),
"agents": ((.[0].agents // {}) * (.[1].agents // {})) "agents": ((.[0].agents // {}) * (.[1].agents // {}))
}' "$home_file" "$cwd_file" }' "$home_file" "$cwd_file"
fi fi
@@ -130,40 +130,40 @@ manifest_require_agent() {
} }
# manifest_env_names <manifest_file> <name> — prints one env-var name per line # manifest_env_names <manifest_file> <name> — prints one env-var name per line
# on stdout (the keys of boxes[agent.box].env, in declaration order). No values. # on stdout (the keys of bottles[agent.bottle].env, in declaration order). No values.
# Prints nothing if the agent has no box or the box has no env. # Prints nothing if the agent has no bottle or the bottle has no env.
manifest_env_names() { manifest_env_names() {
local manifest_file="${1:?manifest_env_names: missing manifest file}" local manifest_file="${1:?manifest_env_names: missing manifest file}"
local name="${2:?manifest_env_names: missing agent name}" local name="${2:?manifest_env_names: missing agent name}"
jq -r --arg n "$name" ' jq -r --arg n "$name" '
.agents[$n].box as $box | .agents[$n].bottle as $bottle |
if ($box == null or $box == "") then empty if ($bottle == null or $bottle == "") then empty
else (.boxes[$box].env // {} | keys_unsorted[]) else (.bottles[$bottle].env // {} | keys_unsorted[])
end end
' "$manifest_file" ' "$manifest_file"
} }
# manifest_env_entry <manifest_file> <agent> <env_name> — prints the raw # manifest_env_entry <manifest_file> <agent> <env_name> — prints the raw
# string value of a single env entry on stdout (no quoting, no JSON # string value of a single env entry on stdout (no quoting, no JSON
# encoding). Env entries live on the agent's box (boxes[agent.box].env). # encoding). Env entries live on the agent's bottle (bottles[agent.bottle].env).
# Used by env_resolve.sh, which classifies the result by sentinel. Dies # Used by env_resolve.sh, which classifies the result by sentinel. Dies
# if the agent has no box, or the entry is not a JSON string; the # if the agent has no bottle, or the entry is not a JSON string; the
# prompt-at-runtime form is "?<message>", not JSON null. # prompt-at-runtime form is "?<message>", not JSON null.
manifest_env_entry() { manifest_env_entry() {
local manifest_file="${1:?manifest_env_entry: missing manifest file}" local manifest_file="${1:?manifest_env_entry: missing manifest file}"
local agent="${2:?manifest_env_entry: missing agent name}" local agent="${2:?manifest_env_entry: missing agent name}"
local var="${3:?manifest_env_entry: missing env var name}" local var="${3:?manifest_env_entry: missing env var name}"
local box local bottle
box="$(jq -r --arg a "$agent" '.agents[$a].box // ""' "$manifest_file")" bottle="$(jq -r --arg a "$agent" '.agents[$a].bottle // ""' "$manifest_file")"
if [ -z "$box" ]; then if [ -z "$bottle" ]; then
die "env entry ${var} for agent ${agent}: agent has no 'box' field" die "env entry ${var} for agent ${agent}: agent has no 'bottle' field"
fi fi
local entry_type local entry_type
entry_type="$(jq -r --arg b "$box" --arg v "$var" '.boxes[$b].env[$v] | type' "$manifest_file")" entry_type="$(jq -r --arg b "$bottle" --arg v "$var" '.bottles[$b].env[$v] | type' "$manifest_file")"
if [ "$entry_type" != "string" ]; then if [ "$entry_type" != "string" ]; then
die "env entry ${var} for agent ${agent} must be a JSON string (was ${entry_type}). Use \"?<message>\" for prompt-at-runtime." die "env entry ${var} for agent ${agent} must be a JSON string (was ${entry_type}). Use \"?<message>\" for prompt-at-runtime."
fi fi
jq -r --arg b "$box" --arg v "$var" '.boxes[$b].env[$v]' "$manifest_file" jq -r --arg b "$bottle" --arg v "$var" '.bottles[$b].env[$v]' "$manifest_file"
} }
# manifest_skills <manifest_file> <name> — prints one skill name per line on # manifest_skills <manifest_file> <name> — prints one skill name per line on
@@ -183,61 +183,61 @@ manifest_prompt() {
jq -r --arg n "$name" '.agents[$n].prompt // ""' "$manifest_file" jq -r --arg n "$name" '.agents[$n].prompt // ""' "$manifest_file"
} }
# manifest_agent_box <manifest_file> <name> — prints the box name referenced # manifest_agent_bottle <manifest_file> <name> — prints the bottle name referenced
# by the agent on stdout, or an empty string if the agent has no "box" field. # by the agent on stdout, or an empty string if the agent has no "bottle" field.
manifest_agent_box() { manifest_agent_bottle() {
local manifest_file="${1:?manifest_agent_box: missing manifest file}" local manifest_file="${1:?manifest_agent_bottle: missing manifest file}"
local name="${2:?manifest_agent_box: missing agent name}" local name="${2:?manifest_agent_bottle: missing agent name}"
jq -r --arg n "$name" '.agents[$n].box // ""' "$manifest_file" jq -r --arg n "$name" '.agents[$n].bottle // ""' "$manifest_file"
} }
# manifest_has_box <manifest_file> <box_name> — returns 0 if the named box # manifest_has_bottle <manifest_file> <bottle_name> — returns 0 if the named bottle
# exists in the manifest, else 1. # exists in the manifest, else 1.
manifest_has_box() { manifest_has_bottle() {
local manifest_file="${1:?manifest_has_box: missing manifest file}" local manifest_file="${1:?manifest_has_bottle: missing manifest file}"
local box_name="${2:?manifest_has_box: missing box name}" local bottle_name="${2:?manifest_has_bottle: missing bottle name}"
jq -e --arg b "$box_name" '.boxes | has($b)' "$manifest_file" >/dev/null 2>&1 jq -e --arg b "$bottle_name" '.bottles | has($b)' "$manifest_file" >/dev/null 2>&1
} }
# manifest_require_box <manifest_file> <box_name> — like manifest_has_box but # manifest_require_bottle <manifest_file> <bottle_name> — like manifest_has_bottle but
# dies with a useful message (and prints available box names) if the box is # dies with a useful message (and prints available bottle names) if the bottle is
# not defined. # not defined.
manifest_require_box() { manifest_require_bottle() {
local manifest_file="${1:?manifest_require_box: missing manifest file}" local manifest_file="${1:?manifest_require_bottle: missing manifest file}"
local box_name="${2:?manifest_require_box: missing box name}" local bottle_name="${2:?manifest_require_bottle: missing bottle name}"
if ! manifest_has_box "$manifest_file" "$box_name"; then if ! manifest_has_bottle "$manifest_file" "$bottle_name"; then
local available local available
available="$(jq -r '.boxes // {} | keys_unsorted | join(", ")' "$manifest_file" 2>/dev/null || echo "")" available="$(jq -r '.bottles // {} | keys_unsorted | join(", ")' "$manifest_file" 2>/dev/null || echo "")"
if [ -n "$available" ]; then if [ -n "$available" ]; then
die "box '${box_name}' not defined in claude-bottle.json. Available boxes: ${available}" die "bottle '${bottle_name}' not defined in claude-bottle.json. Available bottles: ${available}"
else else
die "box '${box_name}' not defined in claude-bottle.json (no boxes defined)." die "bottle '${bottle_name}' not defined in claude-bottle.json (no bottles defined)."
fi fi
fi fi
} }
# manifest_box_ssh <manifest_file> <box_name> — prints one compact JSON object # manifest_bottle_ssh <manifest_file> <bottle_name> — prints one compact JSON object
# per line for each ssh entry in boxes[box_name].ssh. Prints nothing if the # per line for each ssh entry in bottles[bottle_name].ssh. Prints nothing if the
# box has no ssh array or it is empty. # bottle has no ssh array or it is empty.
manifest_box_ssh() { manifest_bottle_ssh() {
local manifest_file="${1:?manifest_box_ssh: missing manifest file}" local manifest_file="${1:?manifest_bottle_ssh: missing manifest file}"
local box_name="${2:?manifest_box_ssh: missing box name}" local bottle_name="${2:?manifest_bottle_ssh: missing bottle name}"
jq -c --arg b "$box_name" '.boxes[$b].ssh // [] | .[]' "$manifest_file" jq -c --arg b "$bottle_name" '.bottles[$b].ssh // [] | .[]' "$manifest_file"
} }
# manifest_ssh <manifest_file> <name> — prints one compact JSON object per line # manifest_ssh <manifest_file> <name> — prints one compact JSON object per line
# for each ssh entry associated with the agent. SSH entries are resolved via # for each ssh entry associated with the agent. SSH entries are resolved via
# the agent's "box" field: if set, entries come from boxes[box].ssh; if the # the agent's "bottle" field: if set, entries come from bottles[bottle].ssh; if the
# agent has no "box" field, prints nothing. # agent has no "bottle" field, prints nothing.
# Each object has: Host, IdentityFile, Hostname, User, Port (required); # Each object has: Host, IdentityFile, Hostname, User, Port (required);
# KnownHostKey (optional). # KnownHostKey (optional).
manifest_ssh() { manifest_ssh() {
local manifest_file="${1:?manifest_ssh: missing manifest file}" local manifest_file="${1:?manifest_ssh: missing manifest file}"
local name="${2:?manifest_ssh: missing agent name}" local name="${2:?manifest_ssh: missing agent name}"
local box local bottle
box="$(jq -r --arg n "$name" '.agents[$n].box // ""' "$manifest_file")" bottle="$(jq -r --arg n "$name" '.agents[$n].bottle // ""' "$manifest_file")"
if [ -z "$box" ]; then if [ -z "$bottle" ]; then
return 0 return 0
fi fi
jq -c --arg b "$box" '.boxes[$b].ssh // [] | .[]' "$manifest_file" jq -c --arg b "$bottle" '.bottles[$b].ssh // [] | .[]' "$manifest_file"
} }