import os, json, glob, subprocess from typing import Optional, Any from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext from lib_py.verify_session import workspace_key class ClaudeAgentAdapter(BaseAgentAdapter): @property def name(self) -> str: return 'claude' @property def own_key(self) -> str: return 'claude_session_id_own' @property def ready_tokens(self) -> str: return 'Anthropic|Assistant|Chat|Welcome|Claude Code|Opus|Sonnet|Haiku' @property def exit_key(self) -> str: return '/exit' @property def delegate_agent_key(self) -> str: return 'claude-code' @property def identity_cache_fields(self) -> tuple: return ('session_id', 'session_jsonl', 'session_size_bytes', 'session_lines') @property def input_prompt(self) -> str: return '❯' @property def input_placeholder(self) -> str: return '' @property def input_rule_pattern(self) -> str: return '─{10,}' def artifact_path(self, uuid: str, ctx: DiscoveryContext) -> str: return f"{ctx.claude_dir}/{ctx.ws_key}/{uuid}.jsonl" def verify_artifact(self, uuid: str, ctx: DiscoveryContext) -> bool: path = self.artifact_path(uuid, ctx) if not os.path.exists(path): return False if ctx.epoch and os.path.getmtime(path) < ctx.epoch: return False try: valid_session = False found_cwd = None with open(path) as f: for _ in range(50): line = f.readline() if not line: break line = line.strip() if not line: continue try: payload = json.loads(line) if payload.get("sessionId") == uuid: valid_session = True if payload.get("cwd"): found_cwd = payload.get("cwd") break except Exception: pass if not valid_session: return False if found_cwd and workspace_key(found_cwd) != workspace_key(ctx.cwd): return False except Exception: return False return True def purge_artifacts(self, uuid: str, ctx: DiscoveryContext) -> list: jsonl = self.artifact_path(uuid, ctx) if os.path.exists(jsonl): os.remove(jsonl) return [jsonl] return [] def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str: if use_wrapper or not session_uuid: return f"{binary} --dangerously-skip-permissions" return f"{binary} --dangerously-skip-permissions --session-id {session_uuid}" def resume_spec(self, binary: str, session_uuid: str, materialized: bool = False) -> str: if materialized and session_uuid: return f"{binary} --dangerously-skip-permissions -r {session_uuid}" elif session_uuid: return f"{binary} --dangerously-skip-permissions --session-id {session_uuid}" return f"{binary} --dangerously-skip-permissions" def auth_ok(self, run_cmd: Optional[Any] = None) -> bool: if run_cmd is not None: try: rc, stdout, stderr = run_cmd(['claude', 'auth', 'status']) return rc == 0 and ('"loggedIn":true' in (stdout or '').replace(' ', '')) except Exception: return False try: res = subprocess.run(['claude', 'auth', 'status'], capture_output=True, text=True) return res.returncode == 0 and ('"loggedIn":true' in (res.stdout or '').replace(' ', '')) except Exception: return False def discover(self, ctx: DiscoveryContext) -> list: key = workspace_key(ctx.workspace) proj = f"{ctx.claude_dir}/{key}" candidates = [] if os.path.isdir(proj): for j in sorted(glob.glob(f"{proj}/*.jsonl"), key=os.path.getmtime, reverse=True): cand = os.path.basename(j)[:-6] if cand and self.verify_artifact(cand, ctx): candidates.append(cand) return candidates