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.
601 lines
26 KiB
Python
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__)
|