refactor!: rename project to bot-bottle
Assisted-by: Codex
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
"""init: interactively create a new agent and add it to bot-bottle.json."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ..log import die, info, warn
|
||||
from ._common import PROG, USER_CWD, read_tty_line
|
||||
|
||||
|
||||
def cmd_init(argv: list[str]) -> int:
|
||||
parser = argparse.ArgumentParser(prog=f"{PROG} init", add_help=True)
|
||||
parser.add_argument("scope", choices=["user", "project"])
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.scope == "user":
|
||||
target_file = Path(os.environ["HOME"]) / "bot-bottle.json"
|
||||
else:
|
||||
target_file = Path(USER_CWD) / "bot-bottle.json"
|
||||
|
||||
print(file=sys.stderr)
|
||||
info(f"bot-bottle init — adding a new agent to {target_file}")
|
||||
print(file=sys.stderr)
|
||||
|
||||
# Agent name
|
||||
agent_name = ""
|
||||
while not agent_name:
|
||||
sys.stderr.write("Agent name: ")
|
||||
sys.stderr.flush()
|
||||
agent_name = read_tty_line().replace(" ", "-")
|
||||
if not agent_name:
|
||||
warn("agent name cannot be empty")
|
||||
|
||||
if not re.match(r"^[a-z0-9][a-z0-9-]*$", agent_name):
|
||||
warn(
|
||||
f"agent name '{agent_name}' contains non-slug characters; "
|
||||
f"it will still work but may cause container naming issues"
|
||||
)
|
||||
|
||||
existing: dict[str, Any] = {}
|
||||
if target_file.is_file():
|
||||
try:
|
||||
existing = json.loads(target_file.read_text())
|
||||
except json.JSONDecodeError:
|
||||
die(f"{target_file} exists but is not valid JSON; fix or remove it first")
|
||||
if agent_name in (existing.get("agents") or {}):
|
||||
sys.stderr.write(
|
||||
f'bot-bottle: agent "{agent_name}" already exists in {target_file}. Overwrite? [y/N] '
|
||||
)
|
||||
sys.stderr.flush()
|
||||
ow = read_tty_line()
|
||||
if ow not in ("y", "Y", "yes", "YES"):
|
||||
info("aborted")
|
||||
return 0
|
||||
|
||||
# Skills
|
||||
print(file=sys.stderr)
|
||||
sys.stderr.write("Skills (space or comma separated, or Enter for none): ")
|
||||
sys.stderr.flush()
|
||||
skills_input = read_tty_line()
|
||||
skill_list: list[str] = []
|
||||
if skills_input:
|
||||
cleaned = skills_input.replace(",", " ")
|
||||
skill_list = [s for s in cleaned.split() if s]
|
||||
|
||||
# Prompt
|
||||
print(file=sys.stderr)
|
||||
info("System prompt — enter text, then a lone '.' on its own line to finish (just '.' to leave empty):")
|
||||
prompt_lines: list[str] = []
|
||||
while True:
|
||||
line = read_tty_line()
|
||||
if line == ".":
|
||||
break
|
||||
prompt_lines.append(line)
|
||||
prompt_content = "\n".join(prompt_lines)
|
||||
|
||||
# Bottle association
|
||||
print(file=sys.stderr)
|
||||
sys.stderr.write("Associate this agent with a bottle? [y/N] ")
|
||||
sys.stderr.flush()
|
||||
bottle_yn = read_tty_line()
|
||||
bottle_name = ""
|
||||
bottle_env: dict[str, str] = {}
|
||||
bottle_ssh: list[dict[str, Any]] = []
|
||||
bottle_exists_already = False
|
||||
if bottle_yn in ("y", "Y", "yes", "YES"):
|
||||
while not bottle_name:
|
||||
sys.stderr.write(" Bottle name: ")
|
||||
sys.stderr.flush()
|
||||
bottle_name = read_tty_line().replace(" ", "-")
|
||||
if not bottle_name:
|
||||
warn("bottle name cannot be empty")
|
||||
|
||||
if bottle_name in (existing.get("bottles") or {}):
|
||||
bottle_exists_already = True
|
||||
info(f"Bottle '{bottle_name}' already exists in {target_file}; agent will reference it.")
|
||||
else:
|
||||
info(f"Creating new bottle '{bottle_name}'.")
|
||||
bottle_env = _prompt_for_env_vars()
|
||||
sys.stderr.write(" Add SSH host entries to this bottle? [y/N] ")
|
||||
sys.stderr.flush()
|
||||
ssh_yn = read_tty_line()
|
||||
if ssh_yn in ("y", "Y", "yes", "YES"):
|
||||
bottle_ssh = _prompt_for_ssh_entries()
|
||||
|
||||
# Build agent JSON
|
||||
agent_def: dict[str, Any] = {"skills": skill_list, "prompt": prompt_content}
|
||||
if bottle_name:
|
||||
agent_def["bottle"] = bottle_name
|
||||
|
||||
merged: dict[str, Any] = {
|
||||
"bottles": dict(existing.get("bottles") or {}),
|
||||
"agents": dict(existing.get("agents") or {}),
|
||||
}
|
||||
if bottle_name and not bottle_exists_already:
|
||||
merged["bottles"][bottle_name] = {"env": bottle_env, "ssh": bottle_ssh}
|
||||
merged["agents"][agent_name] = agent_def
|
||||
|
||||
target_file.write_text(json.dumps(merged, indent=2) + "\n")
|
||||
info(f"Agent '{agent_name}' written to {target_file}.")
|
||||
info(f"Run '{PROG} info {agent_name}' to verify.")
|
||||
print(file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
def _prompt_for_env_vars() -> dict[str, str]:
|
||||
print(file=sys.stderr)
|
||||
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)")
|
||||
out: dict[str, str] = {}
|
||||
while True:
|
||||
print(file=sys.stderr)
|
||||
sys.stderr.write(" Var name (or Enter to finish): ")
|
||||
sys.stderr.flush()
|
||||
vname = read_tty_line()
|
||||
if not vname:
|
||||
break
|
||||
sys.stderr.write(" Mode [secret/interpolated/literal] (default: secret): ")
|
||||
sys.stderr.flush()
|
||||
vmode = read_tty_line() or "secret"
|
||||
if vmode == "secret":
|
||||
sys.stderr.write(f' Prompt message shown to user (default: "enter {vname}"): ')
|
||||
sys.stderr.flush()
|
||||
smsg = read_tty_line()
|
||||
value = f"?{smsg}" if smsg else "?"
|
||||
elif vmode == "interpolated":
|
||||
sys.stderr.write(f" Host env var to read from (default: {vname}): ")
|
||||
sys.stderr.flush()
|
||||
hvar = read_tty_line() or vname
|
||||
value = "${" + hvar + "}"
|
||||
elif vmode == "literal":
|
||||
sys.stderr.write(" Value: ")
|
||||
sys.stderr.flush()
|
||||
value = read_tty_line()
|
||||
else:
|
||||
warn(f"unknown mode '{vmode}'; using secret")
|
||||
value = "?"
|
||||
out[vname] = value
|
||||
return out
|
||||
|
||||
|
||||
def _prompt_for_ssh_entries() -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
while True:
|
||||
print(file=sys.stderr)
|
||||
sys.stderr.write(" SSH Host alias (or Enter to finish): ")
|
||||
sys.stderr.flush()
|
||||
shost = read_tty_line()
|
||||
if not shost:
|
||||
break
|
||||
sys.stderr.write(" Hostname (actual hostname or IP): ")
|
||||
sys.stderr.flush()
|
||||
shostname = read_tty_line()
|
||||
sys.stderr.write(" User: ")
|
||||
sys.stderr.flush()
|
||||
suser = read_tty_line()
|
||||
sys.stderr.write(" Port (default: 22): ")
|
||||
sys.stderr.flush()
|
||||
sport_raw = read_tty_line() or "22"
|
||||
if not sport_raw.isdigit():
|
||||
warn("port must be a number; defaulting to 22")
|
||||
sport_raw = "22"
|
||||
sport = int(sport_raw)
|
||||
sys.stderr.write(" IdentityFile (path to private key on host): ")
|
||||
sys.stderr.flush()
|
||||
sidentity = read_tty_line()
|
||||
sys.stderr.write(" KnownHostKey (optional, Enter to skip): ")
|
||||
sys.stderr.flush()
|
||||
skhk = read_tty_line()
|
||||
|
||||
entry: dict[str, Any] = {
|
||||
"Host": shost,
|
||||
"Hostname": shostname,
|
||||
"User": suser,
|
||||
"Port": sport,
|
||||
"IdentityFile": sidentity,
|
||||
}
|
||||
if skhk:
|
||||
entry["KnownHostKey"] = skhk
|
||||
out.append(entry)
|
||||
return out
|
||||
Reference in New Issue
Block a user