"""REPL slash-command completion and dispatch.""" import getpass import json import os from datetime import datetime, timedelta, timezone from tai_pkg import toolschema from tai_pkg.agent import Agent from tai_pkg.agents import REGISTRY from tai_pkg.consts import Consts from tai_pkg.errors import BackendError from tai_pkg.models import ModelScoring from tai_pkg.render import Ansi from tai_pkg.render import Render from tai_pkg.sandbox import Sandbox from tai_pkg.scheduler import Scheduler from tai_pkg.skills import Skills from tai_pkg.sysinfo import SysInfo from tai_pkg.toolschema import ToolRegistry __all__ = ["Commands"] class Commands: """Commands.""" @staticmethod def complete_command(text, state): options = ["/" + name for name in Consts.COMMANDS if name.startswith(text.lstrip("/"))] return options[state] if state < len(options) else None @staticmethod def handle_command(agent, text): parts = text[1:].split(None, 1) name = parts[0].lower() arg = parts[1].strip() if len(parts) > 1 else "" if name in ("quit", "exit"): return False if name == "help": print(Consts.HELP_TEXT) elif name == "profiles": names = agent.store.list_profiles() for item in names: print("%s %s" % ("*" if item == agent.profile else " ", item)) if not names: print("(no profiles)") elif name == "bots": for item in agent.store.list_bots(agent.profile): nicks = " aka %s" % ", ".join(item["nicknames"]) if item["nicknames"] else "" print("%s %s%s" % ("*" if item["name"] == agent.bot else " ", item["name"], nicks)) elif name == "bot": if not arg: print(Render.paint("use /bot , see /bots", Ansi.RED)) else: print(agent.switch_bot(arg)) elif name == "profile": if not arg: print("current profile: %s (%d chars)" % (agent.profile, len(agent.system_message))) elif not Consts.PROFILE_RE.match(arg): print(Render.paint("invalid profile name", Ansi.RED)) else: agent.switch_profile(arg) elif name == "env": if not arg: print("current environment: %s" % agent.env) elif arg not in ("home", "sandbox"): print(Render.paint("use /env home or /env sandbox", Ansi.RED)) elif arg == "sandbox": try: Sandbox.ensure_box() except BackendError as exc: print(Render.paint("sandbox unavailable: %s" % exc, Ansi.RED)) else: agent.env = "sandbox" print("environment: sandbox (isolated container)") else: agent.env = "home" print("environment: home") elif name == "skills": bits = arg.split() if bits and bits[0] == "install": if len(bits) < 2: print(Render.paint("use /skills install [name=x] [scope=project|home] [mode=copy|symlink] [force]", Ansi.RED)) return True call_args = {"source": bits[1]} for token in bits[2:]: if token == "force": call_args["force"] = True elif "=" in token: key, _, value = token.partition("=") if key in ("name", "scope", "mode"): call_args[key] = value print(agent.tools.dispatch("install_skill", json.dumps(call_args))) return True agent.skills = Skills.discover_skills(agent.config.home, os.getcwd()) agent.apply_system() if not agent.skills: print("(no skills)") for skill_name in sorted(agent.skills): skill = agent.skills[skill_name] print("- %s: %s %s" % (skill_name, skill["description"][:200], Skills.skill_provenance_note(skill))) wanted = sorted(item for item in Consts.SKILL_BLUEPRINTS if item not in agent.skills) if wanted: print("blueprints, built on first load:") for skill_name in wanted: print("- %s: %s" % (skill_name, Consts.SKILL_BLUEPRINTS[skill_name]["hint"])) elif name == "sysinfo": print(SysInfo.collect_sysinfo()) elif name == "models": print(ModelScoring.models_report(agent.chat)) elif name == "secret": sub = arg.split(None, 1) action = sub[0].lower() if sub else "" rest = sub[1].strip() if len(sub) > 1 else "" if action == "set" and rest: if not Consts.SECRET_RE.match(rest): print(Render.paint("invalid secret name, use letters, digits, dot, underscore, hyphen", Ansi.RED)) else: try: value = getpass.getpass("value for '%s': " % rest) except (EOFError, KeyboardInterrupt): print("\naborted") return True if not value: print("empty value, aborted") else: agent.store.save_secret(rest, value, None, None, agent.profile) print("stored secret '%s'" % rest) elif action in ("list", "") and not rest: print(agent.tools.dispatch("list_secrets", "{}")) elif action == "delete" and rest: if agent.store.load_secret(rest, agent.profile) is None: print(Render.paint("unknown secret '%s'" % rest, Ansi.RED)) elif agent.ask_approval("delete secret '%s'" % rest, guidance=False): agent.store.delete_secret(rest, agent.profile) print("deleted secret '%s'" % rest) else: print("aborted") else: print(Render.paint("use /secret set or /secret list or /secret delete ", Ansi.RED)) elif name == "schedule": parts = arg.split(None, 2) if len(parts) != 3 or parts[0] not in ("at", "every"): print(Render.paint("use /schedule at or /schedule every <30s|10m|2h|1d> ", Ansi.RED)) else: mode, when, prompt = parts if mode == "at": try: next_run = Scheduler.parse_schedule_at(when) except ValueError as exc: print(Render.paint("error: %s" % exc, Ansi.RED)) return True every_sec = 0 else: every_sec = Scheduler.parse_duration(when) if every_sec is None or every_sec < 60: print(Render.paint("error: every must be a duration of at least 60s", Ansi.RED)) return True next_run = (datetime.now(timezone.utc) + timedelta(seconds=every_sec)).isoformat() row_id = agent.store.add_schedule("", prompt, agent.profile, every_sec, next_run, 600) print("scheduled #%d" % row_id) elif name == "schedules": print(Scheduler.format_schedule_lines(agent.store.list_schedules(agent.profile), datetime.now(timezone.utc))) elif name == "unschedule": try: row_id = int(arg or "0") except ValueError: print(Render.paint("use /unschedule ", Ansi.RED)) return True if agent.store.remove_schedule(row_id, agent.profile): print("deleted schedule #%d" % row_id) else: print(Render.paint("no schedule #%d" % row_id, Ansi.RED)) elif name == "records": print(agent.tools.dispatch("record_search", json.dumps({"query": arg}))) elif name == "record": bits = arg.split() if not bits: print(Render.paint("use /record [offset]", Ansi.RED)) else: try: offset = int(bits[1]) if len(bits) > 1 else 0 except ValueError: print(Render.paint("offset must be a number", Ansi.RED)) return True print(agent.tools.dispatch("record_read", json.dumps({"id": bits[0], "offset": offset}))) elif name == "graph": if not arg: print(Render.paint("use /graph ", Ansi.RED)) else: print(agent.tools.dispatch("graph_query", json.dumps({"node": arg}))) elif name == "tools": if arg: match = [schema["function"] for schema in toolschema.TOOL_SCHEMAS if schema["function"]["name"] == arg] if not match: print(Render.paint("unknown tool '%s'" % arg, Ansi.RED)) return True print("%s [%s]" % (match[0]["name"], "core" if match[0]["name"] in Consts.CORE_TOOLS else "lazy")) print(match[0]["description"]) print("trigger tags: %s" % ", ".join(Consts.TOOL_TAGS.get(match[0]["name"], ()))) return True names = [schema["function"]["name"] for schema in toolschema.TOOL_SCHEMAS if ToolRegistry.tool_env_ok(schema["function"]["name"])] cores = sorted(tool for tool in names if tool in Consts.CORE_TOOLS) lazy = sorted(tool for tool in names if tool not in Consts.CORE_TOOLS) print("core, always loaded:\n %s" % "\n ".join(cores)) print("lazy, loads when named:\n %s" % "\n ".join(lazy)) elif name == "search": if not arg: print(Render.paint("use /search ", Ansi.RED)) else: print(agent.tools.dispatch("search", json.dumps({"query": arg}))) elif name == "install": bits = arg.split() action = bits[0] if bits else "status" if action not in ("status", "install", "upgrade", "reinstall", "uninstall"): print(Render.paint("use /install [status|install|upgrade|reinstall|uninstall] [targets...]", Ansi.RED)) else: print(agent.tools.dispatch("install", json.dumps({"action": action, "targets": bits[1:] or list(Consts.INSTALL_TARGETS)}))) elif name == "tags": print(agent.tools.dispatch("tags", json.dumps({"prefix": arg} if arg else {}))) elif name == "audit": print(agent.tools.dispatch("audit", json.dumps({"path": arg} if arg else {}))) elif name == "restore": try: row_id = int(arg or "0") except ValueError: print(Render.paint("use /restore ", Ansi.RED)) return True print(agent.tools.dispatch("restore", json.dumps({"id": row_id}))) agent.show_diff() elif name == "release": bits = arg.split(None, 1) if len(bits) != 2 or bits[0] not in ("major", "minor", "patch"): print(Render.paint("use /release ", Ansi.RED)) else: print(agent.tools.dispatch("release", json.dumps({"part": bits[0], "message": bits[1]}))) elif name == "fork": if not arg: print(Render.paint("use /fork ", Ansi.RED)) else: agent_id = REGISTRY.spawn_agent(arg, agent.profile, 600, agent.config, agent.store.seal, agent.depth + 1, agent.runner_override) print("agent %d started, REPL stays free" % agent_id) elif name == "agents": records = REGISTRY.list_agents(agent.profile) if not records: print("(no agents)") for record in records: print("#%d [%s] %ds %s" % (record["id"], record["status"], record["elapsed"], record["task"][:60])) elif name == "agent": if arg == "clear": print("purged %d finished agents" % REGISTRY.clear_agents(agent.profile)) else: try: agent_id = int(arg) except ValueError: print(Render.paint("use /agent or /agent clear", Ansi.RED)) return True status, text = REGISTRY.poll_agent(agent_id, 0, agent.profile) print("[%s]\n%s" % (status, text)) elif name == "compact": agent.messages, changed = Agent.compact_messages(agent.messages, agent.chat) agent.apply_system() print("compacted" if changed else "nothing to compact") elif name == "clear": agent.reset_history() print("history cleared") else: print(Render.paint("unknown command, try /help", Ansi.RED)) return True