feat(agent): add Grok Build TUI adapter, runtime dispatch, and skill support
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import os, json, glob, shutil
|
||||
from typing import Optional, Any
|
||||
from urllib.parse import quote
|
||||
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext
|
||||
from lib_py.verify_session import workspace_key
|
||||
|
||||
|
||||
class GrokAgentAdapter(BaseAgentAdapter):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return 'grok'
|
||||
|
||||
@property
|
||||
def own_key(self) -> str:
|
||||
return 'grok_session_id_own'
|
||||
|
||||
@property
|
||||
def ready_tokens(self) -> str:
|
||||
return 'Grok|xAI|Assistant|❯|>>>'
|
||||
|
||||
@property
|
||||
def exit_key(self) -> str:
|
||||
return '/exit'
|
||||
|
||||
@property
|
||||
def delegate_agent_key(self) -> str:
|
||||
return 'grok-build'
|
||||
|
||||
@property
|
||||
def identity_cache_fields(self) -> tuple:
|
||||
return ('session_id', 'session_jsonl')
|
||||
|
||||
@property
|
||||
def input_prompt(self) -> str:
|
||||
return '❯'
|
||||
|
||||
@property
|
||||
def input_placeholder(self) -> str:
|
||||
return ''
|
||||
|
||||
@property
|
||||
def input_rule_pattern(self) -> str:
|
||||
return '─{10,}'
|
||||
|
||||
def _ws_dir(self, ctx: DiscoveryContext) -> str:
|
||||
return quote(os.path.realpath(ctx.cwd), safe='')
|
||||
|
||||
def artifact_path(self, uuid: str, ctx: DiscoveryContext) -> str:
|
||||
return f"{ctx.home_dir}/.grok/sessions/{self._ws_dir(ctx)}/{uuid}/chat_history.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("session_id") == uuid or payload.get("sessionId") == uuid:
|
||||
valid_session = True
|
||||
if payload.get("cwd"):
|
||||
found_cwd = payload.get("cwd")
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if not valid_session and found_cwd is None:
|
||||
# Directory path itself encodes the workspace and session UUID
|
||||
valid_session = os.path.isdir(os.path.dirname(path))
|
||||
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:
|
||||
purged = []
|
||||
sess_dir = os.path.dirname(self.artifact_path(uuid, ctx))
|
||||
if os.path.isdir(sess_dir):
|
||||
shutil.rmtree(sess_dir, ignore_errors=True)
|
||||
purged.append(sess_dir)
|
||||
elif os.path.exists(self.artifact_path(uuid, ctx)):
|
||||
os.remove(self.artifact_path(uuid, ctx))
|
||||
purged.append(self.artifact_path(uuid, ctx))
|
||||
return purged
|
||||
|
||||
def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str:
|
||||
if session_uuid:
|
||||
return f"{binary} --session-id {session_uuid} --permission-mode bypassPermissions"
|
||||
return f"{binary} --permission-mode bypassPermissions"
|
||||
|
||||
def resume_spec(self, binary: str, session_uuid: str, materialized: bool = False) -> str:
|
||||
if materialized and session_uuid:
|
||||
return f"{binary} --resume {session_uuid} --permission-mode bypassPermissions"
|
||||
if session_uuid:
|
||||
return f"{binary} --session-id {session_uuid} --permission-mode bypassPermissions"
|
||||
return f"{binary} --permission-mode bypassPermissions"
|
||||
|
||||
def auth_ok(self, run_cmd: Optional[Any] = None) -> bool:
|
||||
if os.environ.get("XAI_API_KEY"):
|
||||
return True
|
||||
home = os.environ.get("HOME_DIR") or os.environ.get("HOME") or os.path.expanduser("~")
|
||||
return os.path.exists(f"{home}/.grok/auth.json")
|
||||
|
||||
def discover(self, ctx: DiscoveryContext) -> list:
|
||||
root = f"{ctx.home_dir}/.grok/sessions/{self._ws_dir(ctx)}"
|
||||
if not os.path.isdir(root):
|
||||
return []
|
||||
entries = [d for d in glob.glob(f"{root}/*") if os.path.isdir(d)]
|
||||
entries.sort(key=os.path.getmtime, reverse=True)
|
||||
candidates = []
|
||||
for d in entries:
|
||||
cand = os.path.basename(d)
|
||||
if cand and self.verify_artifact(cand, ctx):
|
||||
candidates.append(cand)
|
||||
return candidates
|
||||
@@ -6,12 +6,14 @@ from lib_py.agents.adapters.claude import ClaudeAgentAdapter
|
||||
from lib_py.agents.adapters.agy import AgyAgentAdapter
|
||||
from lib_py.agents.adapters.hermes import HermesAgentAdapter
|
||||
from lib_py.agents.adapters.cline import ClineAgentAdapter
|
||||
from lib_py.agents.adapters.grok import GrokAgentAdapter
|
||||
|
||||
_ADAPTERS: Dict[str, BaseAgentAdapter] = {
|
||||
'claude': ClaudeAgentAdapter(),
|
||||
'agy': AgyAgentAdapter(),
|
||||
'hermes': HermesAgentAdapter(),
|
||||
'cline': ClineAgentAdapter(),
|
||||
'grok': GrokAgentAdapter(),
|
||||
}
|
||||
|
||||
def get_adapter(agent_name: str) -> Optional[BaseAgentAdapter]:
|
||||
|
||||
@@ -129,7 +129,7 @@ def atomic_dump_yaml_main():
|
||||
if name in old_roles and s.get('role') != old_roles[name]:
|
||||
raise SystemExit(f"VALIDATE: role of session {name!r} cannot be modified from {old_roles[name]!r} to {s.get('role')!r}")
|
||||
|
||||
running_keys = ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'cline_conversation_id_own']
|
||||
running_keys = ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'cline_conversation_id_own', 'grok_session_id_own']
|
||||
id_to_session = {}
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if s.get('status') == 'running':
|
||||
|
||||
@@ -66,7 +66,7 @@ def mam_orchestrator_uuids():
|
||||
def mam_row_own_uuid(row):
|
||||
if not isinstance(row, dict):
|
||||
return None
|
||||
for k in ["claude_session_id_own", "agy_conversation_id_own", "hermes_conversation_id_own", "cline_conversation_id_own"]:
|
||||
for k in ["claude_session_id_own", "agy_conversation_id_own", "hermes_conversation_id_own", "cline_conversation_id_own", "grok_session_id_own"]:
|
||||
v = row.get(k)
|
||||
if v:
|
||||
return v
|
||||
|
||||
@@ -8,7 +8,8 @@ OWN_KEY = {
|
||||
'claude': 'claude_session_id_own',
|
||||
'agy': 'agy_conversation_id_own',
|
||||
'hermes': 'hermes_conversation_id_own',
|
||||
'cline': 'cline_conversation_id_own'
|
||||
'cline': 'cline_conversation_id_own',
|
||||
'grok': 'grok_session_id_own'
|
||||
}
|
||||
|
||||
from lib_py.paths import resolve_home
|
||||
@@ -30,7 +31,7 @@ def find_workspace_uuid_main():
|
||||
if s_item.get('status') == 'running':
|
||||
if target and s_item.get('name') == target:
|
||||
continue
|
||||
for k in ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'cline_conversation_id_own']:
|
||||
for k in ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'cline_conversation_id_own', 'grok_session_id_own']:
|
||||
val = s_item.get(k)
|
||||
if val:
|
||||
running_ids.add(val)
|
||||
|
||||
Reference in New Issue
Block a user