claude-sessions spawn: add --headless/--create for non-interactive use

Claude-Session: https://claude.ai/code/session_01JXneLTuvb5Gxim3DTN2tQa
This commit is contained in:
Paul O'Reilly
2026-10-26 00:26:51 +13:00
parent c32ea467e4
commit d23778942d
3 changed files with 42 additions and 8 deletions

View File

@@ -16,6 +16,10 @@ Python script (`scripts/claude-sessions`, symlinked to `~/sbin/`). Manages Claud
- **Folder browser.** `zenity --file-selection --directory` when `DISPLAY`/`WAYLAND_DISPLAY` is set and zenity exists (has Create Folder). Otherwise a terminal browser: number = open, `u` up, `n` new folder, `/text` filter, `>` `<` page, `h` hidden, `~`/`/path` jump, Enter/`s` select, `q` cancel. - **Folder browser.** `zenity --file-selection --directory` when `DISPLAY`/`WAYLAND_DISPLAY` is set and zenity exists (has Create Folder). Otherwise a terminal browser: number = open, `u` up, `n` new folder, `/text` filter, `>` `<` page, `h` hidden, `~`/`/path` jump, Enter/`s` select, `q` cancel.
- **Topic.** Becomes the tmux session name and the Remote Control name (passed as `--name` to `claude-tmux`). Defaults to the Title-Cased folder name. If that name is already a live tmux session, no default is offered and a unique topic is required. Topics containing `.` or `:` are rejected because tmux rewrites them to `_`. - **Topic.** Becomes the tmux session name and the Remote Control name (passed as `--name` to `claude-tmux`). Defaults to the Title-Cased folder name. If that name is already a live tmux session, no default is offered and a unique topic is required. Topics containing `.` or `:` are rejected because tmux rewrites them to `_`.
## Headless spawn
`claude-sessions spawn <folder> --headless [--name topic] [--create] [--mode m] [--profile p]` — no prompts, no attach, for scripts and the `/spawn-session` skill. Exit 1 = folder missing (or `--create` needs an absolute path); exit 2 = topic in use (default topic is the Title-Cased folder name; no `#N` suffix is added). Without `--headless`, `spawn` behaves as before.
## Gotchas ## Gotchas
- "In use" means a live tmux session. Resumable registry entries do not block a topic. - "In use" means a live tmux session. Resumable registry entries do not block a topic.

View File

@@ -12,14 +12,14 @@ from the Claude app.
- `/spawn-session <project>` — spawn for a named project folder - `/spawn-session <project>` — spawn for a named project folder
- `/spawn-session <project> --mode <mode>` — override engagement mode - `/spawn-session <project> --mode <mode>` — override engagement mode
- `/spawn-session <project> --attach` — attach after spawning
- `/spawn-session` — lists projects and asks which one - `/spawn-session` — lists projects and asks which one
## How it works ## How it works
Thin wrapper over the `claude-tmux` script (in `small-scripts`, symlinked to `~/sbin`). Calls `claude-sessions spawn <folder> --headless [--name topic] [--create]` (since 2026-10-26),
The skill only resolves the project name from `$ARGUMENTS` (or asks) and shells out to which wraps `claude-tmux` (in `small-scripts`, symlinked to `~/sbin`). The skill only resolves
`claude-tmux <project> [flags]`. All real logic — folder resolution, mode selection the folder from `$ARGUMENTS` (or asks). Headless mode never prompts or attaches, and exits 2
if the topic is already a live tmux session (the skill then asks for a unique topic). All real logic — folder resolution, mode selection
(profile `last-mode` unless `--mode`), Title-Casing, ` #N` auto-increment, tmux creation, (profile `last-mode` unless `--mode`), Title-Casing, ` #N` auto-increment, tmux creation,
and launching `claude --remote-control` via `claude-profile` — lives in the script. and launching `claude --remote-control` via `claude-profile` — lives in the script.

View File

@@ -6,7 +6,7 @@ Commands:
(no args) Interactive menu (new session: folder browser + unique topic) (no args) Interactive menu (new session: folder browser + unique topic)
list Show live Claude tmux sessions and resumable registry entries list Show live Claude tmux sessions and resumable registry entries
recover [--days N] Detect sessions active before a restart recover [--days N] Detect sessions active before a restart
spawn <project> Spawn a new session (wraps claude-tmux) spawn <project> Spawn a new session (wraps claude-tmux); --headless for scripted use
resume <session-id> Resume a past session by conversation ID resume <session-id> Resume a past session by conversation ID
register Record a new session in the registry (called by claude-tmux) register Record a new session in the registry (called by claude-tmux)
gc [--days N] Prune stale entries and backfill session IDs gc [--days N] Prune stale entries and backfill session IDs
@@ -650,13 +650,39 @@ def cmd_spawn(args) -> None:
claude_tmux = subprocess.run(["which", "claude-tmux"], capture_output=True, text=True).stdout.strip() claude_tmux = subprocess.run(["which", "claude-tmux"], capture_output=True, text=True).stdout.strip()
if not claude_tmux: if not claude_tmux:
die("claude-tmux not found in PATH") die("claude-tmux not found in PATH")
cmd = [claude_tmux, args.project] project = args.project
name = args.name
if getattr(args, "headless", False):
# Non-interactive: no prompts, no attach. Errors exit non-zero instead of asking.
folder = resolve_project_dir(project)
if folder is None and args.create:
folder = Path(project).expanduser()
if not folder.is_absolute():
die(f"--create needs an absolute path (got {project!r})")
try:
folder.mkdir(parents=True)
except OSError as e:
die(f"Could not create {folder}: {e}")
folder = folder.resolve()
if folder is None:
die(f"Folder not found: {project} (use --create with an absolute path to make it)")
name = name or title_case(folder.name)
if topic_in_use(name, list_tmux_sessions()):
print(f"{RED}Error:{NC} topic '{name}' is already in use — pass a unique --name", file=sys.stderr)
sys.exit(2)
if any(c in name for c in ".:"):
die("Topic must not contain '.' or ':' (tmux rewrites them)")
project = str(folder)
args.attach = False
cmd = [claude_tmux, project]
if args.mode: if args.mode:
cmd += ["--mode", args.mode] cmd += ["--mode", args.mode]
if args.profile: if args.profile:
cmd += ["--profile", args.profile] cmd += ["--profile", args.profile]
if args.name: if name:
cmd += ["--name", args.name] cmd += ["--name", name]
if args.attach: if args.attach:
cmd += ["--attach"] cmd += ["--attach"]
os.execvp(claude_tmux, cmd) os.execvp(claude_tmux, cmd)
@@ -919,6 +945,10 @@ def main() -> None:
sp.add_argument("--profile", default=None) sp.add_argument("--profile", default=None)
sp.add_argument("--name", default=None) sp.add_argument("--name", default=None)
sp.add_argument("--attach", action="store_true") sp.add_argument("--attach", action="store_true")
sp.add_argument("--headless", action="store_true",
help="Non-interactive: never prompt or attach; fail (exit 2) if the topic is in use")
sp.add_argument("--create", action="store_true",
help="With --headless: create the folder (absolute path) if it does not exist")
rsp = sub.add_parser("resume", help="Resume a past conversation by session ID") rsp = sub.add_parser("resume", help="Resume a past conversation by session ID")
rsp.add_argument("session_id", help="Session UUID or prefix") rsp.add_argument("session_id", help="Session UUID or prefix")