4e185fab6b
Remove 35+ unused imports across 20+ files (W0611). Wrap 19 lines to fit under 100 character limit (C0301). Add type casts and annotations in egress_addon_core.py to resolve pyright errors caused by JSON parsing of untyped objects. Key changes: - Remove unused imports (abstractmethod, mock utilities, etc) - Split long lines at logical breaks (method calls, error messages) - Add typing.cast() for proper type inference in JSON parsing - Explicit type annotations for dict/list accesses Results: - Pylint rating: 8.73/10 - egress_addon_core.py: 0 pyright errors (was 15) - All W0611 and C0301 issues fixed Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
221 lines
7.3 KiB
Python
221 lines
7.3 KiB
Python
"""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 '
|
|
f'{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}; "
|
|
f"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
|