Files
tai/tai_pkg/consts.py
T
retoor 8df5cf7a45 Split monolithic tai.py into tai_pkg package, keep tai-orig.py as original
tai.py (8643 lines) is now a thin facade over tai_pkg/ (33 feature modules, one class or feature per file). tai-orig.py preserves the pre-split single-file version byte for byte (sha256 verified against snapshot); import tai and ./tai.py behavior unchanged.

Equivalence proof, all run on this tree immediately pre-commit: differential battery tai-orig.py vs facade 3718 checks 0 failures (surface, pure fuzz, tool dispatch, store/audit, release ops, fake-HOME install roundtrip, CLI runs); test_tai.py 261 OK; test_seal.py 16 OK; --version/--help byte-identical; dir(tai) zero removals.

Fix included: opencode wire-protocol schema echo leaked into replies; opencode_parse now scans with JSONDecoder.raw_decode capped at 200 scans (13 OpencodeParseTests). Test count 261 = 237 HEAD + 8 pre-existing uncommitted ToolArgumentsTests (now committed with this change, disclosed) + 13 OpencodeParseTests + 3 EmbeddedStringsTests. Patch-target-only test migration (24 hunks), zero assertion changes. tai_pkg clean-regenerated after mid-verification tamper was found and reverted.
2026-10-10 19:25:02 +02:00

601 lines
26 KiB
Python

"""Single source of all literal configuration and shared constants."""
import os
import re
__all__ = ["Consts"]
class Consts:
"""All literal configuration and shared constants."""
VERSION = "1.22.0"
WORKER_STEPS = 12
CREATE_SKILL_STEPS = 40
FORK_MAX_DEPTH = 2
MUSE_LABEL = "muse"
MUSE_CHAT_URL = "muse://exec"
MUSE_MODELS_URL = "muse://models"
GROK_LABEL = "grok"
GROK_CHAT_URL = "grok://exec"
GROK_MODELS_URL = "grok://models"
CLAUDE_LABEL = "claude"
CLAUDE_CHAT_URL = "claude://exec"
CLAUDE_MODELS_URL = "claude://models"
CODEX_LABEL = "codex"
CODEX_CHAT_URL = "codex://exec"
CODEX_MODELS_URL = "codex://models"
GEMINI_LABEL = "gemini"
GEMINI_CHAT_URL = "gemini://exec"
GEMINI_MODELS_URL = "gemini://models"
OLLAMA_LABEL = "ollama"
OLLAMA_BASE = "http://localhost:11434/v1"
OLLAMA_LIST_TTL = 600
CLI_CHAT_URLS = (MUSE_CHAT_URL, GROK_CHAT_URL, CLAUDE_CHAT_URL, CODEX_CHAT_URL, GEMINI_CHAT_URL)
MUSE_MAX_STEPS = 1
MUSE_SESSIONS_FILE = "muse_sessions.json"
MUSE_LOCKS_DIR = "muse_locks"
MUSE_WORKER_TTL = 86400
MUSE_REAP_GRACE = 30
OPENROUTER_LABEL = "openrouter"
OPENROUTER_BASE = "https://openrouter.ai/api/v1"
OPENROUTER_MODEL = "deepseek/deepseek-v4.1-flash"
OPENCODE_LABEL = "opencode"
OPENCODE_CHAT_URL = "opencode://run"
OPENCODE_MODELS_URL = "opencode://models"
OPENCODE_AGENT = "plan"
OPENCODE_PARSE_MAX_SCANS = 200
DEVPLACE_LABEL = "devplace"
DEVPLACE_BASE = "https://devplace.net/openai/v1"
DEVPLACE_FREE_ROUTES = ("free", "devii")
ZEN_LABEL = "zen"
ZEN_BASE = "https://opencode.ai/zen/v1"
ZEN_FREE_MODELS = ("mimo-v2.6-flash-free", "nemotron-3.5-lightning-free", "nemotron-3-ultra-free", "muse-spark-1.3-contributor-free", "muse-spark-1.2-contributor-free", "jev-1.13-free", "exo-free", "space-bunny-free", "longcat-2.5-preview-free", "ling-3.1-flash-free", "ling-3.0-flash-fin-free", "fledge-alpha-free")
POLLINATIONS_LABEL = "pollinations"
POLLINATIONS_BASE = "https://text.pollinations.ai/openai/v1"
POLLINATIONS_MODELS = ("openai",)
OPENCODE_USER_AGENT = "opencode/1.18.29 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.15"
OPENCODE_CLIENT_NAME = "cli"
MODEL_HEALTH_FILE = "model_health.json"
MODEL_NEUTRAL_REWARD = 0.5
MODEL_WEIGHT_PRIOR = 2.0
MODEL_SPEED_REF_TPS = 25.0
MODEL_LATENCY_REF_MS = 8000.0
MODEL_CIRCUIT_THRESHOLD = 3
MODEL_CIRCUIT_COOLDOWN = 300.0
RETRY_BACKOFF = (2, 5, 10, 20, 30, 60)
MODEL_LIST_TIMEOUT = 8
OPENROUTER_FREE_CACHE_TTL = 3600
RSEARCH_BASE = "https://rsearch.app.molodetz.nl"
FETCH_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS")
CURL_HINT = "hint: use the web_fetch tool for HTTP(S) (methods, headers, bodies, status) instead of curl/wget via shell"
DEFAULT_MODEL = "muse"
DEFAULT_VOICE = "en-US-EmmaMultilingualNeural"
DEFAULT_PASSPHRASE = "tai-default-insecure-change-me"
BOX_IMAGE = "tai-box:latest"
BOX_NAME = "tai-box"
TELEGRAM_API = "https://api.telegram.org/bot"
TELEGRAM_FILE_API = "https://api.telegram.org/file/bot"
SKILL_NAME_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$")
BASHRC_MARK_BEGIN = "# tai command-not-found hook - start"
BASHRC_MARK_END = "# tai command-not-found hook - end"
BASHRC_BLOCK = (
BASHRC_MARK_BEGIN + "\n"
"alias tai=\"$HOME/.local/bin/tai.py\"\n"
"command_not_found_handle() {\n"
" \"$HOME/.local/bin/tai.py\" \"$@\"\n"
" return $?\n"
"}\n" + BASHRC_MARK_END + "\n"
)
ZSH_NOTFOUND_BLOCK = (
BASHRC_MARK_BEGIN + "\n"
"alias tai=\"$HOME/.local/bin/tai.py\"\n"
"command_not_found_handler() {\n"
" \"$HOME/.local/bin/tai.py\" \"$@\"\n"
" return $?\n"
"}\n" + BASHRC_MARK_END + "\n"
)
FISH_NOTFOUND_BLOCK = (
BASHRC_MARK_BEGIN + "\n"
"function tai\n"
" \"$HOME/.local/bin/tai.py\" $argv\n"
"end\n"
"function fish_command_not_found\n"
" \"$HOME/.local/bin/tai.py\" $argv\n"
"end\n" + BASHRC_MARK_END + "\n"
)
BOX_CONTAINERFILE = """FROM python:3.12-slim
ENV DEBIAN_FRONTEND=noninteractive PIP_NO_CACHE_DIR=1
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg espeak-ng python3-venv && rm -rf /var/lib/apt/lists/*
RUN python -m venv /box/venv && /box/venv/bin/pip install --no-cache-dir faster-whisper edge-tts
RUN /box/venv/bin/python -c "from faster_whisper import WhisperModel; WhisperModel('tiny', device='cpu', compute_type='int8')"
COPY stt.py /box/stt.py
COPY tts.py /box/tts.py
CMD ["sleep", "infinity"]
"""
BOX_PYTHON = "/box/venv/bin/python"
BOX_STT = """import sys
from faster_whisper import WhisperModel
data = sys.stdin.buffer.read()
with open("/tmp/in.audio", "wb") as handle:
handle.write(data)
model = WhisperModel("tiny", device="cpu", compute_type="int8")
segments, _info = model.transcribe("/tmp/in.audio")
print(" ".join(segment.text for segment in segments).strip(), flush=True)
"""
BOX_TTS = """import asyncio
import sys
import edge_tts
async def speak(text, voice):
talk = edge_tts.Communicate(text, voice or "en-US-EmmaMultilingualNeural")
await talk.save("/tmp/out.mp3")
text = sys.stdin.read().strip()
voice = sys.argv[1] if len(sys.argv) > 1 else ""
asyncio.run(speak(text, voice))
sys.stdout.buffer.write(open("/tmp/out.mp3", "rb").read())
"""
TELEGRAM_UNIT = """[Unit]
Description=tai telegram bot
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=%%h
ExecStart=%s --telegram
EnvironmentFile=%s
Restart=on-failure
RestartSec=10
[Install]
WantedBy=default.target
"""
SCHEDULER_UNIT = """[Unit]
Description=tai scheduler
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=%%h
ExecStart=%s --scheduler
Restart=on-failure
RestartSec=10
[Install]
WantedBy=default.target
"""
INSTALL_TARGETS = ("binary", "bash-hook", "zsh-hook", "fish-hook", "plexer-hook", "zsh-plexer-hook", "fish-plexer-hook", "venv", "scheduler-service", "telegram-service", "container")
CONTEXT_CAP = 32000
COMPACT_RATIO = 0.8
KEEP_TURNS = 6
MAX_STEPS = 25
TOTAL_STEP_CAP = 500
LOOP_NUDGE_AT = 3
LOOP_STOP_AT = 5
SYSTEM_MAX_CHARS = 12000
SHELL_TIMEOUT = 120
SCHEDULER_INTERVAL = 30
HTTP_TIMEOUT = 60
STREAM_TIMEOUT = 300
EDGE_HOST = "speech.platform.bing.com"
EDGE_WS_PATH = "/consumer/speech/synthesize/readaloud/edge/v1"
EDGE_TRUSTED_TOKEN = "6A5AA1D4EAFF4E9FB37E23D68491D6F4"
EDGE_CHROMIUM = "143.0.3650.75"
PROFILE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$")
BOT_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,31}$")
BOT_MENTION_RE = re.compile(r"^@([A-Za-z0-9][A-Za-z0-9_-]{0,31})\s+(.*)$", re.DOTALL)
SECRET_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$")
LEGACY_PASSWORD_NOTE = "especially passwords (collect and keep every password the user shares), preferences, or standing behavior changes"
VAULT_MEMORY_NOTE = "preferences, or standing behavior changes. Store passwords, tokens, and secrets with store_secret and reference them by name; never write secret values into memory"
SAFE_COMMANDS = ("ls", "pwd", "echo", "cat", "head", "tail", "grep", "find", "wc", "sort", "uniq", "diff", "file", "stat", "date", "whoami", "uname", "lsb_release")
RECORDERS = (("arecord", ("arecord", "-q", "-d", "{seconds}", "-f", "cd", "-t", "wav", "{path}")), ("rec", ("rec", "-q", "{path}", "trim", "0", "{seconds}")), ("ffmpeg", ("ffmpeg", "-y", "-v", "quiet", "-f", "alsa", "-i", "default", "-t", "{seconds}", "{path}")))
TRANSCRIBERS = ("whisper-cpp", "whisper-cli", "whisper", "faster-whisper")
SYSINFO_TIMEOUT = 10
SYSINFO_BINARIES = ("tmux", "git", "ffmpeg", "arecord", "sox", "whisper-cpp", "whisper", "systemctl", "podman", "docker", "muse", "grok", "opencode", "claude", "codex", "gemini", "ollama")
DEFAULT_SYSTEM = (
"You are tai, a professional autonomous assistant. You act with tools, verify results, and report concisely. "
"Rules: prefer the smallest change that solves the task; never silently ignore errors; ask when requirements are ambiguous; "
"keep answers short and factual. Tools: shell and file tools for the local machine, web_search and web_fetch for the internet, "
"web_fetch handles every HTTP method with headers and bodies so never curl or wget through shell, "
"speak for voice output, recall to search past session memory. Memory: call remember whenever you learn durable facts, "
"preferences, or standing behavior changes. Store passwords, tokens, and secrets with store_secret and reference them by name; "
"never write secret values into memory. Name and value suffice for secrets; never pester the user for more. Call remember with a forget instruction to drop outdated knowledge. Delegation: do quick, interactive, or memory-changing work yourself; fork background subagents for independent, long, or context-heavy work and collect them with poll; schedule future work instead of waiting on it. "
"Records: save big or durable findings with record_save and tag generously; large tool outputs spill to mem:<hash> ids automatically, page them with record_read, connect related nodes with graph_link. "
"Tags: strict rules, lowercase singular only, shortest common word for the subject, one canonical tag per subject, at most 5 per item, always reuse existing tags from the tags tool instead of inventing synonyms. Content words matching known tags attach automatically, and new records link to recent same-tag records. "
"Lazy tools: only everyday tools load by default; name any other tool or topic in your reply text and it becomes callable on your next step. "
"Bots: one identity holds many bots; a bot is only a system message plus its own history, all vault data is shared. @name routes one turn to another bot and files the exchange in both histories; /bot switches, create_bot makes bots. "
"Terminal: the user's question is very likely about what is visible in their terminal, especially right after a terminal capture or when launched with a single question straight from a shell; resolve vague references (this, that, it, the error) against that content, capture it with get_current_terminal_content instead of asking, and answer from what you see. "
"Retrieval: use search first for anything stored: one ranked full-text query across records, events, and audit plus graph expansion beats paging and guessing. Chain tool calls until the task is done."
)
MERGE_SYSTEM = (
"You maintain an AI assistant system prompt. You receive the current system prompt and one memory instruction. "
"Rewrite the system prompt to incorporate the instruction: add new facts, update changed behavior, or remove forgotten items. "
"Never persist passwords, tokens, or other secret values; if the instruction holds one, note only that a secret exists and must be stored with store_secret. "
"Preserve everything unrelated. Keep it organized with short sections. Output ONLY the rewritten system prompt, no explanation, no code fences."
)
COMPACT_SYSTEM = (
"You compress assistant session history into a dense handoff summary. Cover: goal, completed work with outcomes, current state, "
"key decisions, files touched, errors and fixes, pending next steps. Output only the summary, no preamble."
)
AUTONOMOUS_NOTE = (
"Autonomous mode: never ask the user anything this session. When requirements are unclear, blocked, or need input, "
"research deeply with web_search, web_fetch, shell, and subagents, then decide and proceed. State assumptions briefly in your reply."
)
HELP_TEXT = (
"commands:\n"
" /profile [name] show current profile or switch to it\n"
" /profiles list all profiles, current marked with *\n"
" /bots list bots, current marked with *\n"
" /bot <name> switch bot, history resumes\n"
" /env [home|sandbox] show or switch execution environment\n"
" /skills list loaded skills with provenance\n"
" /skills install ... install an existing skill (copy or symlink)\n"
" /secret manage sealed secrets (set|list|delete)\n"
" /sysinfo show host environment checks\n"
" /models show model roster with speed health\n"
" /schedule at|every schedule a prompt for later\n"
" /schedules list scheduled prompts\n"
" /unschedule <id> delete a scheduled prompt\n"
" /records [query] search saved records\n"
" /record <id> read one record page\n"
" /graph <node> show node neighborhood\n"
" /tags [prefix] list tags with usage counts\n"
" /tools [name] show core and lazy tools\n"
" /install ... install status, install, upgrade, reinstall\n"
" /search <query> ranked search over everything\n"
" /audit [path] show file audit trail\n"
" /restore <id> restore a file from an audit row\n"
" /release part msg bump version, back up, log message\n"
" /fork <task> spawn a background subagent, REPL stays free\n"
" /agents list background subagents\n"
" /agent <id|clear> show one result, or purge finished agents\n"
" /compact compress history into a summary\n"
" /clear drop history, keep system message\n"
" /help this text\n"
" /quit exit\n"
"anything else is sent to the agent."
)
SPILL_LIMIT = 6000
RECORD_KINDS = ("note", "output", "file", "research", "transcript", "termlog")
AUDIT_MAX_CHARS = 20000
FILE_RECORD_MAX = 50000
SHELL_SNAPSHOT_MAX_FILES = 25
BACKUP_KEEP = 10
_UNSET = object()
SHELL_SEGMENT = (";", "&&", "||", "|", "&", "(", ")")
SHELL_WRAPPERS = ("sudo", "command", "env", "nice", "nohup", "setsid")
CHUNK_MAX_LINES = 60
CHUNK_OVERLAP_LINES = 3
CHUNK_MAX_CHARS = 4000
VERSION_RE = re.compile(r"^(?:[ \t]*)VERSION = \"(\d+)\.(\d+)\.(\d+)\"$", re.MULTILINE)
STREAM_HEIGHT = 4
DIFF_CONTEXT = 3
DIFF_MAX_LINES = 120
BG_ADD = "\033[48;5;28m"
BG_DEL = "\033[48;5;88m"
FG_ORANGE = "\033[38;5;214m"
PY_KEYWORDS = ("False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with", "yield", "match", "case")
PY_TOKEN_RE = re.compile(r"(?P<string>\"\"\"(?:\\.|[^\\])*?\"\"\"|'''(?:\\.|[^\\])*?'''|\"(?:\\.|[^\"\\\n])*\"|'(?:\\.|[^'\\\n])*')|(?P<comment>#[^\n]*)|(?P<number>\b\d[\d._]*(?:[eE][+-]?\d+)?[jJ]?\b)|(?P<keyword>\b(?:%s)\b)|(?P<decorator>@[A-Za-z_][\w.]*)|(?P<defname>(?<=def )[A-Za-z_]\w*|(?<=class )[A-Za-z_]\w*)" % "|".join(PY_KEYWORDS))
ANSI_RE = re.compile(r"\033\[[0-9;]*m")
MD_BULLETS = ("•", "◦", "▪")
EXTERNAL_SKILL_DIRNAMES = (os.path.join(".claude", "skills"), os.path.join(".agents", "skills"))
SKILL_INSTALL_META_DIRNAME = ".installs"
SKILL_BLUEPRINTS = {
"bot-creator": {
"hint": "build a dedicated chat bot from a tai.py variant",
"scope": "project",
"brief": "Deep-research preferred chat-bot features first: transport (Telegram long-poll versus webhook), command routing, per-user allowlists, rate limits, and safe restarts. Also research safe techniques for scripting huge single-file programs: exact-match edits, atomic temp-plus-rename writes, and versioned backups before every reshape. Then copy tai.py to a variant path and reshape the copy into a dedicated bot, editing efficiently and keeping the audit trail intact.",
},
"api-client": {
"hint": "build a resilient REST/OpenAPI client",
"scope": "project",
"brief": "Deep-research resilient HTTP client design first: retries with backoff, timeouts, pagination, auth header handling, and error classification. Then write a skill that builds small stdlib-only API clients from a base URL and an endpoint list.",
},
"web-researcher": {
"hint": "deep research with triangulated sources",
"scope": "project",
"brief": "Deep-research professional open-source research methodology first: query expansion, source triangulation, recency checks, and claim grading. Then write a skill that turns a question into a sourced brief with confidence levels.",
},
"pdf-forms": {
"hint": "fill PDF forms and extract field data",
"scope": "project",
"brief": "Deep-research PDF form handling with freely available tooling first: AcroForm field discovery, filling, flattening, and text extraction. Then write a skill that fills forms and extracts field data from this machine's installed tools.",
},
"data-wrangler": {
"hint": "reshape CSV, JSON, and SQLite data",
"scope": "project",
"brief": "Deep-research tabular data reshaping first: CSV dialects, JSON normalization, SQLite import ergonomics, and streaming for large files. Then write a skill that reshapes CSV, JSON, and SQLite inputs using stdlib-only scripts.",
},
"home-sysadmin": {
"hint": "routine Linux host care and triage",
"scope": "home",
"brief": "Deep-research routine single-host Linux care first: disk, memory, service health, log triage, and backup verification. Then write a skill that runs a host checkup and reports findings with suggested fixes.",
},
}
BOX_OPENCODE_NODE_VERSION = "20.19.2"
# Installed on demand, not baked into BOX_CONTAINERFILE: the box exists for
# STT/TTS today and most installs already have one running, so this only
# pays Node's weight (~800MB, dominated by opencode-ai's own node_modules,
# not the runtime itself) on the one machine, the one time, it's actually
# needed as a last-resort chat backend (see ChatClient.all_candidates()).
# The official nodejs.org tarball, not apt: Debian's nodejs/npm packages
# drag in webpack/babel/jest as transitive deps (measured: 1.49GB vs 971MB
# for the exact same opencode-ai install) and are not relevant here at all.
BOX_OPENCODE_INSTALL_SCRIPT = (
"set -e; "
"ARCH=\"$(dpkg --print-architecture)\"; "
"case \"$ARCH\" in amd64) NODE_ARCH=x64;; arm64) NODE_ARCH=arm64;; armhf) NODE_ARCH=armv7l;; i386) NODE_ARCH=x86;; *) echo \"unsupported arch $ARCH\" >&2; exit 1;; esac; "
"apt-get update -qq && apt-get install -y -qq --no-install-recommends curl xz-utils ca-certificates; "
"curl -fsSL \"https://nodejs.org/dist/v%(ver)s/node-v%(ver)s-linux-$NODE_ARCH.tar.xz\" -o /tmp/node.tar.xz; "
"tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1; "
"rm /tmp/node.tar.xz; "
"apt-get purge -y -qq curl xz-utils; apt-get autoremove -y -qq; rm -rf /var/lib/apt/lists/*; "
"npm install -g opencode-ai --no-fund --no-audit"
) % {"ver": BOX_OPENCODE_NODE_VERSION}
MEM_EVENTS_CAP = 10000
SCHEMA = (
("events", ("id INTEGER PRIMARY KEY", "profile TEXT", "ts TEXT", "role TEXT", "kind TEXT", "text TEXT", "tags TEXT"), ()),
("secrets", ("profile TEXT", "name TEXT", "value TEXT", "meta TEXT", "updated TEXT"), ("profile", "name")),
("schedules", ("id INTEGER PRIMARY KEY", "name TEXT", "prompt TEXT", "profile TEXT", "every_sec INTEGER", "next_run TEXT", "timeout INTEGER", "status TEXT", "last_status TEXT", "last_result TEXT", "created TEXT", "updated TEXT"), ()),
("records", ("id TEXT PRIMARY KEY", "kind TEXT", "profile TEXT", "title TEXT", "content TEXT", "size INTEGER", "reads INTEGER", "created TEXT", "updated TEXT"), ()),
("tags", ("item TEXT", "tag TEXT", "profile TEXT"), ("item", "tag", "profile")),
("edges", ("src TEXT", "dst TEXT", "relation TEXT", "profile TEXT", "created TEXT"), ("src", "dst", "relation", "profile")),
("audit", ("id INTEGER PRIMARY KEY", "profile TEXT", "ts TEXT", "actor TEXT", "action TEXT", "path TEXT", "message TEXT", "old_size INTEGER", "new_size INTEGER", "old TEXT", "new TEXT", "tags TEXT"), ()),
("chunks", ("id INTEGER PRIMARY KEY", "parent_kind TEXT", "parent_id TEXT", "idx INTEGER", "text TEXT", "profile TEXT", "created TEXT"), ()),
)
SCHEMA_INDEXES = (
("idx_events_profile", "events(profile)"),
("idx_tags_tag", "tags(tag)"),
("idx_edges_src", "edges(src)"),
("idx_edges_dst", "edges(dst)"),
("idx_audit_path", "audit(path)"),
("idx_tags_profile", "tags(profile)"),
("idx_edges_profile", "edges(profile)"),
("idx_records_profile", "records(profile)"),
("idx_audit_profile", "audit(profile)"),
("idx_chunks_parent", "chunks(parent_kind, parent_id)"),
("idx_chunks_profile", "chunks(profile)"),
)
CORE_TOOLS = ("shell", "read_file", "write_file", "remember", "recall", "load_skill", "fork", "poll")
LAZY_NAME_STOP = ("get", "set", "list", "add")
TOOL_TAGS = {
"shell": ("run", "execute", "command", "bash", "terminal", "script"),
"read_file": ("read", "open", "view", "contents"),
"write_file": ("write", "create", "overwrite"),
"edit_file": ("edit", "change", "modify", "replace", "patch", "fix", "alter"),
"web_search": ("search", "web", "internet", "lookup", "google"),
"web_fetch": ("fetch", "download", "url", "link", "page", "curl", "wget"),
"speak": ("speak", "say", "voice", "talk", "tts"),
"listen": ("listen", "hear", "microphone", "mic", "transcribe", "stt", "dictate"),
"remember": ("remember", "memorize", "preference"),
"recall": ("recall", "memory", "history", "past"),
"load_skill": ("skill", "capability"),
"get_current_terminal_content": ("terminal", "tmux", "pane", "scrollback", "screen"),
"export_terminal_log": ("terminal", "tmux", "export", "log", "capture", "history", "rag", "index"),
"plexer_send": ("plexer", "terminal", "tmux", "keys", "send-keys", "type", "keystroke"),
"plexer_read": ("plexer", "terminal", "tmux", "pane", "read", "capture"),
"plexer_type": ("plexer", "terminal", "tmux", "type", "animate", "keystroke", "demo"),
"plexer_status": ("plexer", "terminal", "tmux", "status", "session"),
"fork": ("fork", "subagent", "background", "parallel", "delegate", "spawn"),
"poll": ("poll", "collect"),
"sysinfo": ("sysinfo", "system", "host", "machine", "hardware", "specs", "installed", "python", "container"),
"create_skill": ("skill", "create", "author", "blueprint"),
"install_skill": ("skill", "install", "import", "adopt", "copy", "symlink"),
"store_secret": ("secret", "password", "token", "credential", "passwd", "vault", "apikey"),
"list_secrets": ("secrets", "vault", "credentials"),
"delete_secret": ("secret", "remove", "revoke"),
"schedule": ("schedule", "cron", "appointment", "reminder", "recurring", "periodic", "later", "daily", "hourly", "interval"),
"unschedule": ("unschedule", "cancel"),
"schedules": ("schedules", "upcoming", "planned", "agenda"),
"record_save": ("record", "note", "document", "memo"),
"record_read": ("record", "page"),
"record_search": ("record", "find"),
"record_delete": ("record", "remove"),
"graph_link": ("graph", "link", "connect", "relate", "edge", "relation"),
"graph_query": ("graph", "neighbors", "neighborhood", "traverse", "related"),
"delete_file": ("delete", "remove", "erase"),
"audit": ("audit", "trail", "changes", "blame", "history", "version"),
"restore": ("restore", "undo", "revert", "recover", "rollback", "undelete", "previous"),
"release": ("release", "version", "bump", "changelog", "publish"),
"tags": ("tags", "tag", "label", "categorize", "taxonomy", "vocabulary"),
"install": ("install", "uninstall", "reinstall", "upgrade", "setup", "service", "systemd", "venv", "hook", "deploy", "zsh", "fish"),
"create_bot": ("bot", "bots", "persona", "character", "mention"),
"search": ("search", "find", "lookup", "everything"),
}
TAG_AUTO_SKIP = ("record", "file")
TAG_AUTO_MAX = 5
TAG_LINK_MAX = 3
SINGULAR_KEEP = ("news", "means", "series", "species", "physics")
PLEXER_DIR_NAME = "plexer"
PLEXER_PREFIX_KEY = b"\x01" # Ctrl+A, deliberately distinct from tmux's default Ctrl+B
PLEXER_SOCKET_ENV = "TAI_PLEXER_SOCKET"
PLEXER_ACTIVE_ENV = "TAI_PLEXER_ACTIVE"
PLEXER_RECV_MAX = 1 << 20
PLEXER_BASHRC_MARK_BEGIN = "# tai plexer hook - start"
PLEXER_BASHRC_MARK_END = "# tai plexer hook - end"
PLEXER_BASHRC_BLOCK = (
PLEXER_BASHRC_MARK_BEGIN + "\n"
"if [ -z \"$TAI_PLEXER_ACTIVE\" ] && [ -z \"$TMUX\" ] && [ ! -e \"$HOME/.tai-no-plexer\" ] && case \"$-\" in *i*) true;; *) false;; esac; then\n"
" if [ -t 0 ] && [ -t 1 ]; then\n"
" exec \"$HOME/.local/bin/tai.py\" --plexer-wrap\n"
" fi\n"
"fi\n"
+ PLEXER_BASHRC_MARK_END + "\n"
)
FISH_PLEXER_BLOCK = (
PLEXER_BASHRC_MARK_BEGIN + "\n"
"if status is-interactive\n"
"and test -z \"$TAI_PLEXER_ACTIVE\"\n"
"and test -z \"$TMUX\"\n"
"and not test -e \"$HOME/.tai-no-plexer\"\n"
"and isatty stdin\n"
"and isatty stdout\n"
" exec \"$HOME/.local/bin/tai.py\" --plexer-wrap\n"
"end\n"
+ PLEXER_BASHRC_MARK_END + "\n"
)
COMMANDS = ("help", "profile", "profiles", "bots", "bot", "env", "skills", "sysinfo", "models", "secret", "schedule", "schedules", "unschedule", "records", "record", "graph", "tags", "tools", "install", "search", "audit", "restore", "release", "fork", "agents", "agent", "compact", "clear", "quit", "exit")
TAI_SCRIPT = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), "tai.py"))
TAI_CONSTS = os.path.abspath(__file__)