From fa6eda0bbd09035871018613893f1cf428e13475 Mon Sep 17 00:00:00 2001 From: Natalie Date: Sun, 7 Jun 2026 19:31:01 -0700 Subject: [PATCH] =?UTF-8?q?feat(@scripts):=20=E2=9C=A8=20add=20account-wid?= =?UTF-8?q?e=20env=20list=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- README.md | 19 ++++++++ bin/claude-rc | 5 +++ bin/claude-rc-envs | 110 +++++++++++++++++++++++++++++++++++++++++++++ bin/crc | 8 ++++ 4 files changed, 142 insertions(+) create mode 100755 bin/claude-rc-envs diff --git a/README.md b/README.md index f0fd03d..0aa8f63 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,27 @@ claude-rc rm # disable + unregister claude-rc sync # reconcile units to the registry claude-rc logs [-f] # journal claude-rc restart|stop|start +claude-rc envs # account-wide environment list (all hosts) ``` +## Account-wide view — `claude-rc-envs` + +`claude-rc list`/`status` only know about *this host's* units. To see every +Remote Control environment across all hosts (exactly what the claude.ai/code +picker shows — including orphans and cloud envs), query the API: + +```sh +crc status # or: crc envs / claude-rc envs / rc envs +claude-rc-envs # state, host, project, env id, dir +claude-rc-envs --json +claude-rc-envs rm # delete an environment (e.g. an orphan) +claude-rc-envs archive +``` + +Reads the OAuth token from the macOS Keychain or `~/.claude/.credentials.json` +and hits `GET https://api.anthropic.com/v1/environments` +(`anthropic-beta: environments-2025-11-01`). + Defaults (override via env in a unit drop-in): - `CLAUDE_RC_SPAWN=worktree` — isolated git worktree per spawned session. - `CLAUDE_RC_PERM=bypassPermissions` — spawned sessions skip permission prompts. diff --git a/bin/claude-rc b/bin/claude-rc index 08feff2..4eb6876 100755 --- a/bin/claude-rc +++ b/bin/claude-rc @@ -81,6 +81,11 @@ case "$cmd" in printf '%-16s %-10s %s\n' "$n" "$(uc is-active "$TPL$n" 2>/dev/null || echo -)" "$(reg_dir "$n")" done ;; + envs|environments) + # Account-wide environment list across ALL hosts (the claude.ai/code + # picker view) — not just this host's units. + exec claude-rc-envs "$@" + ;; status|st) show() { env=$(url_of "$1") diff --git a/bin/claude-rc-envs b/bin/claude-rc-envs new file mode 100755 index 0000000..6c81ba9 --- /dev/null +++ b/bin/claude-rc-envs @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""claude-rc-envs — list / inspect / remove Claude Remote Control environments. + +Account-wide view (the same list the claude.ai/code environment picker shows), +across every host. Reads the OAuth token from the macOS Keychain (Claude Code) +or ~/.claude/.credentials.json. + +Usage: + claude-rc-envs # table: state, host, project, dir + claude-rc-envs --json # raw JSON + claude-rc-envs rm # delete an environment (e.g. an orphan) + claude-rc-envs archive # archive instead of delete +""" +import json +import os +import platform +import subprocess +import sys +import urllib.error +import urllib.request + +API = "https://api.anthropic.com/v1/environments" +BETA = "environments-2025-11-01" + + +def token() -> str: + if platform.system() == "Darwin": + try: + out = subprocess.run( + ["security", "find-generic-password", "-s", "Claude Code-credentials", "-w"], + capture_output=True, text=True, timeout=10, + ) + if out.returncode == 0 and out.stdout.strip(): + return json.loads(out.stdout)["claudeAiOauth"]["accessToken"] + except Exception: + pass + path = os.path.expanduser("~/.claude/.credentials.json") + if os.path.exists(path): + with open(path) as fh: + return json.load(fh)["claudeAiOauth"]["accessToken"] + sys.exit("claude-rc-envs: no Claude credentials found (Keychain or ~/.claude/.credentials.json)") + + +def call(method: str, url: str) -> dict: + req = urllib.request.Request(url, method=method, headers={ + "Authorization": f"Bearer {token()}", + "anthropic-beta": BETA, + "anthropic-version": "2023-06-01", + "content-type": "application/json", + }) + try: + with urllib.request.urlopen(req, timeout=30) as resp: + body = resp.read() + return json.loads(body) if body else {} + except urllib.error.HTTPError as e: + sys.exit(f"claude-rc-envs: HTTP {e.code} {e.reason}: {e.read().decode()[:200]}") + except urllib.error.URLError as e: + sys.exit(f"claude-rc-envs: network error: {e.reason}") + + +def fetch() -> list: + return call("GET", API).get("data", []) + + +def row(e: dict): + c = e.get("config", {}) + bridge = c.get("type") == "bridge" + host = c.get("machine_name") or ("cloud" if not bridge else "?") + name = e.get("name", "") + project = name.split(":")[1] if bridge and name.count(":") >= 1 else name + return e.get("state", "?"), host, project, e.get("id", ""), c.get("directory", "") + + +def list_envs(): + rows = [row(e) for e in fetch()] + if not rows: + print("(no environments)") + return + wh = max(4, *(len(r[1]) for r in rows)) + wp = max(7, *(len(r[2]) for r in rows)) + print(f"{'STATE':8} {'HOST':{wh}} {'PROJECT':{wp}} {'ENV ID':20} DIR") + for state, host, project, env_id, directory in rows: + print(f"{state:8} {host:{wh}} {project:{wp}} {env_id:20} {directory}") + + +def main(): + args = sys.argv[1:] + if not args: + return list_envs() + cmd = args[0] + if cmd in ("--json", "json"): + print(json.dumps(fetch(), indent=2)) + elif cmd in ("rm", "delete", "--rm"): + if len(args) < 2: + sys.exit("usage: claude-rc-envs rm ") + call("DELETE", f"{API}/{args[1]}") + print(f"removed {args[1]}") + elif cmd in ("archive", "--archive"): + if len(args) < 2: + sys.exit("usage: claude-rc-envs archive ") + call("POST", f"{API}/{args[1]}/archive") + print(f"archived {args[1]}") + elif cmd in ("ls", "list", "status", "envs"): + list_envs() + else: + sys.exit(f"claude-rc-envs: unknown command '{cmd}' (try: ls | --json | rm | archive )") + + +if __name__ == "__main__": + main() diff --git a/bin/crc b/bin/crc index b993ab5..6a323e6 100755 --- a/bin/crc +++ b/bin/crc @@ -42,6 +42,14 @@ rc_args='' usage() { sed -n '2,31p' "$0" | sed 's/^# \{0,1\}//'; } +# Account-wide environment list (the claude.ai/code picker view, every host). +# crc | crc status | crc envs → list +# crc envs rm | archive | --json +case "${1:-}" in + ''|status) exec claude-rc-envs ;; + envs|ls) shift; exec claude-rc-envs "$@" ;; +esac + while [ $# -gt 0 ]; do case "$1" in -h|--help) usage; exit 0 ;;