4032e04a9c
Replaces the cwd-hash identity with a random 5-char base36 suffix per launch, so two simultaneous `start <agent>` invocations against the same cwd no longer collide on container names. Each launch is its own bottle. State carries metadata: every prepare step writes ~/.claude-bottle/state/<identity>/metadata.json with the (agent_name, cwd, copy_cwd, started_at) the bottle was launched with. The new `cli.py resume <identity>` reads this metadata and re-launches a bottle pinned to the same identity — picking up the per-bottle Dockerfile (from a prior capability-block apply) and the transcript snapshot under the same state dir. - bottle_state.py: bottle_identity(agent_name) drops the cwd param and gains a random suffix; BottleMetadata dataclass + read/write/metadata_path helpers. - BottleSpec gains an optional identity field — resume sets it to pin the identity; start leaves it empty so prepare mints fresh. - prepare.py: writes metadata at launch time; uses spec.identity if provided (resume) else bottle_identity(agent_name) (fresh start). - start.py: extracted _launch_bottle from cmd_start so resume can share the launch core; prints `./cli.py resume <identity>` hint at session end. - cli/resume.py (new): reads metadata, reconstructs BottleSpec with the recorded identity + cwd, delegates to _launch_bottle. Errors clearly when no state exists for the given identity. - cli/__init__.py: registers `resume` in COMMANDS + usage. - dashboard.py: capability-block approval status line now appends the `resume <identity>` hint so the operator can copy-paste the rebuild command without leaving the TUI. Closes the rebuild loop in PRD 0016: agent calls capability-block → operator approves → bottle torn down with state preserved → status line shows resume command → operator runs it → replacement bottle boots with the new Dockerfile and prior transcript. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
74 lines
2.2 KiB
Python
74 lines
2.2 KiB
Python
"""Main CLI dispatcher.
|
|
|
|
Commands: cleanup, dashboard, edit, info, init, list, resume, start
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
|
|
from ..log import Die, die
|
|
from ._common import PROG
|
|
from . import list as _list_mod
|
|
from .cleanup import cmd_cleanup
|
|
from .dashboard import cmd_dashboard
|
|
from .edit import cmd_edit
|
|
from .info import cmd_info
|
|
from .init import cmd_init
|
|
from .resume import cmd_resume
|
|
from .start import cmd_start
|
|
|
|
cmd_list = _list_mod.cmd_list
|
|
|
|
COMMANDS = {
|
|
"cleanup": cmd_cleanup,
|
|
"dashboard": cmd_dashboard,
|
|
"edit": cmd_edit,
|
|
"info": cmd_info,
|
|
"init": cmd_init,
|
|
"list": cmd_list,
|
|
"resume": cmd_resume,
|
|
"start": cmd_start,
|
|
}
|
|
|
|
|
|
def usage() -> None:
|
|
sys.stderr.write(f"usage: {PROG} <command> [args...]\n\n")
|
|
sys.stderr.write("Commands:\n")
|
|
sys.stderr.write(" cleanup stop and remove all active claude-bottle containers\n")
|
|
sys.stderr.write(" dashboard view + approve/modify/reject pending supervise proposals (PRD 0013)\n")
|
|
sys.stderr.write(" edit open an agent in vim for editing\n")
|
|
sys.stderr.write(" info print env, skills, and prompt details for a named agent\n")
|
|
sys.stderr.write(" init interactively create a new agent and add it to claude-bottle.json\n")
|
|
sys.stderr.write(" list list available agents or active containers\n")
|
|
sys.stderr.write(" resume re-launch a bottle by its identity (continues state from PRD 0016)\n")
|
|
sys.stderr.write(" start boot a container for a named agent and attach an interactive session\n\n")
|
|
sys.stderr.write(f"Run '{PROG} <command> --help' for command-specific usage.\n")
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
if argv is None:
|
|
argv = sys.argv[1:]
|
|
if not argv:
|
|
usage()
|
|
return 2
|
|
command = argv[0]
|
|
rest = argv[1:]
|
|
if command in ("-h", "--help"):
|
|
usage()
|
|
return 0
|
|
handler = COMMANDS.get(command)
|
|
if handler is None:
|
|
usage()
|
|
die(f"unknown command: {command}")
|
|
try:
|
|
return handler(rest) or 0
|
|
except Die as e:
|
|
return e.code if isinstance(e.code, int) else 1
|
|
except KeyboardInterrupt:
|
|
return 130
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|