import { z } from "zod" export const EFFORTS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const export const MODES = ["manual", "edit", "auto", "plan"] as const /** The personality presets (harness/prompts/personality/, shared with the web UI), then custom. */ export const PERSONALITY_PRESETS = ["concise", "pragmatic", "optimistic", "funny", "formal", "socratic"] as const export const PERSONALITIES = [...PERSONALITY_PRESETS, "custom"] as const /** Older names of a mode, still read wherever a mode is written by hand (harness spec v1 renamed * `unrestricted` to `auto`, LLeMbas's word). */ export const MODE_ALIASES: Record = { unrestricted: "auto" } export const ACTIONS = ["allow", "ask", "deny"] as const export type Effort = (typeof EFFORTS)[number] export type Mode = (typeof MODES)[number] export type Action = (typeof ACTIONS)[number] /** Permission keys the harness spec renamed (v1; 1.1.0 folded todo into the plan tools, whose key * is `plan`), read as the new ones in a config's `permission:` and in the spec's own defaults. */ export const PERMISSION_ALIASES: Record = { question: "ask_user", plan_exit: "plan_submit", websearch: "web_search", webfetch: "web_fetch", todo: "plan", bash_output: "job_output", bash_list: "job_list", bash_kill: "job_stop" } export function renamePermissionKeys(p: Record): Record { const out: Record = {} for (const [k, v] of Object.entries(p)) { const to = PERMISSION_ALIASES[k] // The new name written as well wins: it is the one somebody wrote on purpose. if (to) out[to] ??= v else out[k] = v } return out } /** A mode as written (a config file, --mode, /mode, an agent file, the settings tool), with the * old names read as the new; undefined when it is not one. */ export function asMode(v: unknown): Mode | undefined { if (typeof v !== "string") return undefined const t = v.trim() if ((MODES as readonly string[]).includes(t)) return t as Mode return MODE_ALIASES[t] } const Tristate = z.enum(["auto", "on", "off"]) /** A URL — or a string that becomes one when its {env:}/{file:} is filled in. Files are validated * before substitution, so the placeholder must pass here; the loader checks the result again. */ const Url = z.string().refine((s) => /\{(env|file):[^}]+\}/.test(s) || URL.canParse(s), { message: "Invalid URL" }) const Tls = z.strictObject({ ca: z.string().optional(), insecure: z.boolean().optional() }) export const ModelSpec = z.strictObject({ name: z.string().optional().describe("A display name; the id is what is sent."), family: z.string().optional().describe("Picks the prompt overlay: anthropic | gpt | gemini | local. Guessed from the id when absent."), context: z.number().int().nonnegative().optional().describe("Total context window in tokens. 0 / absent = unknown: no percentage, no auto-compaction."), max_output: z.number().int().positive().optional().describe("Max tokens per reply."), temperature: z.number().min(0).max(2).optional().describe("Sampling temperature; absent = the server's default."), top_p: z.number().min(0).max(1).optional().describe("Nucleus sampling; absent = the server's default."), efforts: z.array(z.enum(EFFORTS)).optional().describe("The model's own effort vocabulary, in offering order. [] — it takes none (its one level is `default`)."), thinking_switch: z.boolean().optional().describe("The server turns thinking off for a request with chat_template_kwargs.enable_thinking false: only then is the level `off` offered."), effort: z.union([z.enum(EFFORTS), z.literal("off"), z.literal("default")]).optional().describe("The level a conversation starts with, when the model offers it: an effort, `off` (thinking switched off, with thinking_switch) or `default` (nothing sent, for a model that takes no efforts)."), effort_style: z.enum(["top", "kwargs", "both"]).optional().describe("Where the effort goes in a request: top-level, in chat_template_kwargs, or both (the default: llama.cpp drops top-level)."), vision: z.boolean().optional().describe("The model reads images: @image files, pasted paths and view_image."), tools: z.boolean().optional().describe("false: a model without tool calls — it can only talk."), headers: z.record(z.string(), z.string()).optional().describe("Extra HTTP headers for this model's requests."), body: z.record(z.string(), z.unknown()).optional().describe("Merged into every request body for this model."), notes: z.string().max(300).optional().describe("What this model is good at, in a line: the model in use is told which others there are, for handing work over (task) or switching."), single_session: z.boolean().optional().describe("The server serves one request at a time: a subagent on this same model would take its only slot and push the session's cached prompt out, so it is refused (name a model on another server instead). The same as parallel: 1."), parallel: z.number().int().positive().optional().describe("How many requests the model's server serves at once (capacity.parallel); wins over single_session. Absent with no single_session: no limit (a hosted API). The session's model is its own subagents' model only when it serves at least 2."), group: z.string().optional().describe("Models sharing one GPU server that holds one model at a time (llama-swap), as the LLeMbas server says (capacity.group): a subagent on another model of the same group would unload this one, so it is refused."), }) export type ModelSpec = z.infer /** Server oddities; auto is right almost always. */ export const Quirks = z .strictObject({ stream_usage: Tristate.optional().describe("Ask for token usage in the stream (stream_options); auto learns when a server refuses it."), prompt_progress: Tristate.optional().describe("Ask how far the server is through reading the prompt (llama.cpp's return_progress), shown while it reads; auto learns when a server refuses it, and one that ignores it shows nothing."), think_tags: Tristate.optional().describe("… in a reply is taken as reasoning; off leaves it as text."), max_tokens_field: z.enum(["max_tokens", "max_completion_tokens"]).optional().describe("OpenAI reasoning models refuse `max_tokens`; most local servers only know it."), }) .optional() .describe("Server oddities; auto is right almost always.") /** The LLeMbas server as LLeMbas CLI speaks to it: its OpenAI-shaped `/v1` with this machine's * token, and the models it serves. Not something a person writes — custom connections were removed * in 2.0 (CLI rule 2: the CLI is LLeMbas-server-specific); `lembas login` writes the one `webui` * entry below, and the loader turns it into this (lembas/webui.ts, expandWebui). */ export interface Connection { base_url: string api_key?: string tls?: z.infer /** Seconds of silence allowed in a reply (default 600). */ timeout?: number quirks?: z.infer /** Extra headers and body fields for every request (none from a login; tests use them). */ headers?: Record body?: Record models: Record } /** A LLeMbas instance: its address and this machine's key, and nothing else. The models, * their settings, and — where config.yaml says `provider: webui` — the voice and the web search come * from the instance at every start. `lembas login` writes it. */ export const WebuiConnection = z.strictObject({ type: z.literal("webui").describe("A LLeMbas instance: its models, their settings, its voice and its web search."), url: Url.describe("The instance's address, e.g. https://ai.example.org (no /v1)."), api_key: z.string().optional().describe("This machine's token, as login wrote it: {file:~/.config/lembas/lembas/.key}."), tls: Tls.optional().describe("For an instance behind a private CA: `ca`, the PEM file that signed its certificate."), timeout: z.number().positive().optional().describe("Seconds of silence allowed in a reply, as for any connection (default 600)."), quirks: Quirks, models: z.record(z.string(), ModelSpec).optional().describe("Only to change what the instance says about a model, here: each entry goes over the instance's. Usually absent."), }) export type WebuiConnection = z.infer /** connections.yaml: the login, as `lembas login` writes it. An entry of any other kind — a custom * connection from 1.x — is read past with a warning (config/load.ts), not refused, so an old file * still starts. */ export const ConnectionsFile = z.strictObject({ connections: z .record(z.string(), WebuiConnection) .default({}) .describe("The LLeMbas server this machine is logged in to, as `lembas login` writes it: its address and this machine's token. Its models come from it at every start."), }) export type ConnectionsFile = z.infer const ActionSchema = z.enum(ACTIONS) /** `tool: action` or `tool: { pattern: action }` — order matters, last match wins. */ export const PermissionConfig = z.record(z.string(), z.union([ActionSchema, z.record(z.string(), ActionSchema)])) export type PermissionConfig = z.infer const Section = z.record(z.string(), z.unknown()) const VoiceEndpoint = { base_url: z.string().optional(), api_key: z.string().optional(), key_cmd: z.string().optional(), tls: Tls.optional(), timeout: z.number().positive().optional(), } /** `provider: webui`: through a LLeMbas instance's own speech servers, with the account's * voice, speed and language. */ const WebuiVoice = { connection: z.string().optional().describe("webui: which webui connection (default: the only one there is)."), } const SttEndpoint = z.strictObject({ provider: z.enum(["openai", "whispercpp", "webui"]).optional().describe("openai: POST {base_url}/audio/transcriptions (OpenAI, whisper.cpp's server with its OpenAI path, faster-whisper servers). whispercpp: POST {base_url}/inference (whisper.cpp's own). webui: the LLeMbas instance's."), ...VoiceEndpoint, ...WebuiVoice, model: z.string().optional(), language: z.string().optional(), prompt: z.string().optional().describe("Words it should expect: names, jargon."), }) const TtsEndpoint = z.strictObject({ provider: z.enum(["openai", "piper-http", "piper-cli", "webui"]).optional().describe("openai: POST {base_url}/audio/speech (OpenAI, Kokoro-FastAPI). piper-http: POST {base_url} with {text} (piper's http_server). piper-cli: the piper program. webui: the LLeMbas instance's, in the account's voice."), ...VoiceEndpoint, ...WebuiVoice, model: z.string().optional(), voice: z.string().optional(), speed: z.number().min(0.25).max(4).optional(), command: z.string().optional().describe("piper-cli: the program (default piper) and the voice model file (.onnx)."), model_path: z.string().optional(), }) /** Voice: speech to text, text to speech, and how to record and play. Global only — it * carries endpoints and keys, like a connection. */ export const Voice = z.strictObject({ stt: SttEndpoint.extend({ fallback: SttEndpoint.optional().describe("Used when the provider above cannot be reached — for webui, when the instance is down. Same fields.") }).optional(), tts: TtsEndpoint.extend({ fallback: TtsEndpoint.optional().describe("Used when the provider above cannot be reached — for webui, when the instance is down. Same fields.") }).optional(), record_key: z.string().optional().describe("The key that starts and stops recording (default ctrl+t)."), record_mode: z.enum(["toggle", "hold"]).optional().describe("toggle (default): press to start, press again (or silence) to stop. hold: also stops when the key is let go, in terminals that report it (the kitty keyboard protocol)."), submit: z.enum(["draft", "send"]).optional().describe("What happens to what you said: draft (default) puts it in the prompt to check; send sends it."), speak: z.boolean().optional().describe("Speak every reply (default false; /speak toggles it)."), silence_seconds: z.number().positive().optional().describe("Seconds of silence after speech that end a recording (default 3), and the longest one (120)."), max_seconds: z.number().positive().optional(), silence_threshold: z.number().positive().optional().describe("Loudness (RMS of 16-bit samples) below which it counts as silence (default 200)."), recorder: z.enum(["auto", "arecord", "pw-record", "parec", "sox", "ffmpeg"]).optional().describe("auto, or a program: arecord, pw-record, parec, sox, ffmpeg. `recorder_command` instead: any command printing raw 16 kHz mono signed 16-bit little-endian audio to stdout."), recorder_command: z.array(z.string()).min(1).optional(), player: z.enum(["auto", "pw-play", "paplay", "aplay", "ffplay", "mpv", "afplay"]).optional().describe("auto, or a program: pw-play, paplay, aplay, ffplay, mpv, afplay. `player_command` instead: any command; the WAV file's path is added at the end."), player_command: z.array(z.string()).min(1).optional(), }) export type Voice = z.infer /** One MCP server: `command` (stdio, a local process) or `url` (streamable HTTP, SSE fallback). */ export const McpServer = z .strictObject({ command: z.union([z.string(), z.array(z.string()).min(1)]).optional().describe("A local server: the program (and its arguments, as a list). Either this or url."), args: z.array(z.string()).optional(), env: z.record(z.string(), z.string()).optional().describe("Environment for the local server; {env:}/{file:} work here."), cwd: z.string().optional(), url: z.string().optional().describe("A remote server: its streamable HTTP (or SSE) address. Either this or command."), headers: z.record(z.string(), z.string()).optional().describe("Extra HTTP headers, e.g. Authorization: Bearer {env:TOKEN}."), transport: z.enum(["http", "sse"]).optional().describe("http (default: streamable HTTP, falling back to SSE) or sse."), oauth: z .union([ z.literal(false), z.strictObject({ client_id: z.string().optional(), client_secret: z.string().optional(), scope: z.string().optional(), callback_port: z.number().int().positive().optional() }), ]) .optional() .describe("OAuth for a remote server: on by default when the server asks for it; false turns it off."), tls: Tls.optional(), enabled: z.boolean().optional().describe("false: configured but not started (/mcp on starts it)."), timeout: z.number().positive().optional().describe("Seconds: one tool call (default 120), connecting (default 30)."), connect_timeout: z.number().positive().optional(), tools: z.strictObject({ include: z.array(z.string()).optional(), exclude: z.array(z.string()).optional() }).optional().describe("Which of its tools to offer: include wins; names or globs (`create_*`)."), prompts: z.boolean().optional().describe("Offer its prompts as /server:prompt commands (default true)."), resources: z.boolean().optional().describe("Offer list/read tools for its resources, if it has any (default true)."), instructions: z.boolean().optional().describe("Put the server's own instructions into the system prompt (default true)."), }) .refine((s) => (s.command === undefined) !== (s.url === undefined), { message: "give either command (a local server) or url (a remote one)" }) export type McpServer = z.infer export const UPDATE_CHANNELS = ["stable", "beta"] as const /** Where updates come from and how they are checked. Global only: a project never decides * what binary runs. */ export const Update = z.strictObject({ channel: z.enum(UPDATE_CHANNELS).optional().describe("stable (default): releases vX.Y.Z only. beta: also vX.Y.Z-beta.N — whichever is newest."), auto: z.enum(["install", "notify", "off"]).optional().describe("On start: install (default) — fetch, verify and install a newer release, then say so; notify — only say one is out; off — never look."), source: z .strictObject({ type: z.enum(["github", "gitea", "static"]).describe("github or gitea (Forgejo too): their releases API. static: any web directory with files `stable` and `beta` naming a tag, and the assets under /."), url: Url.optional().describe("gitea: the forge (https://git.example.org). github: the API (default https://api.github.com). static: the directory."), repo: z.string().regex(/^(?!\.\.?\/)[\w.-]+\/(?!\.\.?$)[\w.-]+$/).optional().describe("owner/name, for github and gitea."), token: z.string().optional().describe("For a private repository: {env:NAME} or {file:~/path}, never pasted. Or key_cmd."), key_cmd: z.string().optional(), tls: Tls.optional(), }) .optional() .describe("Default: the public LLeMbas CLI releases on GitHub."), public_key: z.union([z.string(), z.array(z.string())]).optional().describe("The ssh-ed25519 key(s) a release's SHA256SUMS.sig must be signed with — instead of LLeMbas CLI's own. For your own builds."), verify: z.enum(["signature", "checksum"]).optional().describe("signature (default): SHA256SUMS must carry a valid signature. checksum: checksums only — for a source of your own that does not sign."), }) export type Update = z.infer export const Config = z.strictObject({ model: z.string().optional().describe("The model a session starts with: one the LLeMbas server you are logged in to serves, by its id there (`deepseek/deepseek-flash`). Default: the server's default for your account."), small_model: z.string().optional().describe("The model for small jobs — session titles, commit messages, by its id on the server. Default: the session's model."), titles: z.enum(["model", "prompt"]).optional().describe("Session titles: model (default) — named from the first prompt, then by the model after its first reply; prompt — only from the first prompt, no extra request."), mode: z.enum(MODES).optional().describe("The permission mode a session starts in (default manual)."), theme: z.string().optional().describe("Colours: moria (default), shire, lembas, lembas-light, hermes, mono, or a themes/.yaml of your own."), theme_background: z.enum(["theme", "terminal"]).optional().describe("theme (default): the theme paints the whole screen; terminal: the terminal's own background shows, and the theme colours only text, panels and accents."), skin: z.string().optional().describe("The old name of `theme`; still read when `theme` is not set."), busy_input: z.enum(["steer", "queue"]).optional().describe("A message sent while the agent works: steer (default) — it goes in at the next step, after the running tools finish, and the agent carries on with it; queue — it is sent as the next prompt once the task is done."), mouse: z.boolean().optional().describe("false: leave the mouse to the terminal — its own selection and Ctrl+Shift+C work without Shift, and the wheel no longer scrolls the transcript (pgup/pgdn do). Default true."), notify: z.boolean().optional().describe("When a reply finishes, or the agent waits for you, while the terminal is not focused: ring the bell and ask the terminal for a desktop notification (OSC 9) naming the session — never what the reply says. Default true; false: neither."), icons: z.enum(["emoji", "plain"]).optional().describe("emoji (default), or plain: one-column symbols for a terminal font that has no emoji."), personality: z .enum(PERSONALITIES) .or(z.literal("")) .optional() .describe("How the agent talks: concise, pragmatic, optimistic, funny, formal, socratic, custom (personality_custom's text), or empty for none (the default). The same presets as the LLeMbas web UI; appended last to the system prompt, under \"## Personality\"."), personality_custom: z.string().max(1500).optional().describe("The personality, in your own words: used when personality is custom (at most 1500 characters)."), memory: z .strictObject({ enabled: z.boolean().optional().describe("false: no memory tool and nothing remembered in the prompt."), memory_chars: z.number().int().positive().optional().describe("Size limits in characters: memory 2200, user 1375 by default."), user_chars: z.number().int().positive().optional(), }) .optional() .describe("Persistent memory (MEMORY.md, USER.md in ~/.config/lembas/memory/)."), skills: z .strictObject({ external_dirs: z.array(z.string()).optional().describe("More directories of skills, read-only; a skill of the same name elsewhere wins."), disabled: z.array(z.string()).optional().describe("Skill names not to offer."), }) .optional(), permission: PermissionConfig.optional().describe("Rules per tool: `tool: allow|ask|deny` or `tool: { pattern: action }`; the last match wins."), hardline_extra: z.array(z.string()).optional().describe("Global config only: extra never-allowed bash regexes."), hardline_disable: z.array(z.string()).optional().describe("Global config only: ids of built-in hardline rules to switch off."), limits: z .strictObject({ steps: z.number().int().positive().optional().describe("Most model steps in one prompt (default 200) — a runaway backstop, not a budget."), bash_timeout: z.number().positive().optional().describe("Seconds a bash command may run in the foreground before it goes on in the background as a job (default 120)."), jobs: z.number().int().positive().optional().describe("Background commands one session may have running at once (default 8)."), subagent_steps: z.number().int().positive().optional().describe("Most model steps in one subagent run (default 50) — a runaway backstop, not a budget."), wall_seconds: z.number().positive().optional().describe("Budget for one prompt: seconds of work, not counting time spent waiting for you. Past it the model answers from what it has. Unset: none."), output_bytes: z.number().int().positive().optional().describe("Budget for one prompt: bytes of tool output read. Unset: none."), completion_tokens: z.number().int().positive().optional().describe("Budget for one prompt: tokens the model writes. Unset: none."), }) .optional(), question: z.strictObject({ unattended: z.enum(["recommended", "first", "fail"]).optional().describe("With nobody to answer (headless): take the recommended option, the first, or fail.") }).optional(), compaction: z.strictObject({ auto_at: z.number().min(0.1).max(1).optional() }).optional().describe("Automatic context management: past auto_at of the window, the conversation is compacted. auto_at: 1 turns it off. (Old tool outputs are never cut afterwards: that would make the server read the whole conversation again.)"), instructions: z.string().max(4000).optional().describe("Custom instructions: how you want to be helped, in your own words (at most 4000 characters). Appended near the end of the system prompt, under \"## How the person you are talking to wants to be helped\" — as in the LLeMbas web UI. (A list of files here, from an older config, is read as instruction_files.)"), instruction_files: z.array(z.string()).optional().describe("More instruction files for the system prompt, besides AGENTS.md / CLAUDE.md."), search: z .strictObject({ order: z.array(z.enum(["webui", "searxng", "firecrawl", "ddg"])).optional().describe("Providers to try, in order; the first that answers wins. Default: those configured (webui first), then ddg. webui: the LLeMbas instance's own search."), max_results: z.number().int().min(1).max(30).optional().describe("Results per search (default 8)."), searxng: z.strictObject({ base_url: Url, api_key: z.string().optional(), tls: Tls.optional(), categories: z.string().optional(), language: z.string().optional() }).optional(), firecrawl: z.strictObject({ base_url: Url.optional(), api_key: z.string().optional(), tls: Tls.optional() }).optional().describe("API (api.firecrawl.dev, with a key) or self-hosted (its own URL, no key needed)."), ddg: z.strictObject({ region: z.string().optional(), safe: z.enum(["strict", "moderate", "off"]).optional() }).optional(), webui: z.strictObject({ connection: z.string().optional().describe("Which webui connection (default: the only one there is).") }).optional().describe("Search through a LLeMbas instance: its provider, its settings, its keys."), fetch: z.enum(["local", "firecrawl", "webui"]).optional().describe("How web_fetch reads a page: here, through Firecrawl's scrape, or through the LLeMbas instance."), }) .optional(), voice: Voice.optional().describe("Voice in and out. Global only — it carries endpoints and keys, like a connection."), mcp: z.record(z.string(), McpServer).optional().describe("MCP servers by name. `{env:}`/`{file:}` are filled in per server (a missing one disables that server only). A project's servers count only once the project is trusted."), knowledge: z.array(z.string()).optional().describe("The knowledge bases searched (lembas kb); absent: all of them. A project's own config narrows it to what that project needs."), embedding: z.string().optional().describe("Global config only. An embedding model the LLeMbas server serves, by its id there: knowledge bases are then searched by meaning as well as by words. lembas kb reindex after changing it."), effort: z.union([z.enum(EFFORTS), z.literal("off"), z.literal("default")]).optional().describe("The effort level a session starts with, when the model offers it: an effort, off (thinking switched off, on a model with the switch) or default; otherwise the model's own start level."), agents: z .strictObject({ min: z.number().int().min(1).optional().describe("Subagents, Min: the agents at work at least, this agent included (default 1 — 1 is this agent alone). 2 or more: it works through subagents from the start."), max: z.number().int().min(1).optional().describe("Subagents, Max: the agents at work at most, this agent included (default 4 — this agent and up to 3 subagents). 1: this agent alone, no subagents."), cap: z.number().int().min(1).optional().describe("Global config only. The highest max a session may set (default 32)."), }) .optional() .describe("Subagents (harness team.md, subagents.md): how many agents a session may have at work, counting the session's own agent — Subagents: Min 1, Max 4 by default; 1 is this agent alone. Offered where subagents are on and a model could serve one: the session's, when it serves two requests at once, or another model off its one-model server. /team size and /effort (one range, Min and Max) change it for the session."), subagents: z.boolean().optional().describe("false: no subagents — no task tool and no team."), settings_tool: z.enum(["ask", "allow", "off"]).optional().describe("Global config only. The agent's settings tool: ask (default) — each change is approved like any tool call; allow — changes go through without asking (a less strict mode still asks); off — no settings tool."), update: Update.optional().describe("Global config only. Updating LLeMbas CLI itself."), prototypes: z.boolean().optional().describe("Global config only. Offer the model prototypes: a complete HTML page in a `prototype-chat` or `prototype-page` fence, saved as a file under the state directory (prototypes//) and listed by /prototype; the LLeMbas web UI renders the same fences live. Default false."), library: z.enum(["local", "lembas"]).optional().describe("Global config only. Where memory lives. lembas (the default when the login may reach it): the account's memory records on the LLeMbas server, through its /mcp — the same records its chats have. local: this machine's memory files. Notes, knowledge bases and skills are this machine's either way."), remote: z .strictObject({ enabled: z.boolean().optional().describe("Accept work from a LLeMbas instance this machine is logged in to (lembas service). Default false."), roots: z.array(z.string()).optional().describe("The directories a remote session may be opened in, and anything under them. Empty: none."), max_mode: z.enum(MODES).optional().describe("The most permissive mode a remote session may run in (default edit). auto only when set here."), approval_timeout: z.number().int().positive().optional().describe("Seconds an approval waits for an answer from the web before it counts as a denial (default 600)."), require_trust: z.boolean().optional().describe("Only projects trusted on this machine (default true)."), terminal: z.boolean().optional().describe("Let the web UI open a terminal here (LLeMbas's terminal panel on a chat on this device): a login shell as this user, in a directory remote sessions may use. Default false."), scheduled: z.boolean().optional().describe("Accept scheduled work (harness acp.md, spec 3.5.0): let your schedules on the LLeMbas instance run here as agent turns, in the folders remote work may use, in the schedule's mode (Edit unless it says Auto) lowered to max_mode. Default false. lembas service scheduled on|off."), terminal_integration: z.boolean().optional().describe("Shell integration in those terminals (bash, zsh, fish): after your own startup files, the shell marks each prompt, command and exit status (OSC 133) and its directory (OSC 7), so the web UI shows the directory, a mark beside each finished command, and can jump between commands. false: the shell starts as it is. Default true."), }) .optional() .describe("Global config only. What a LLeMbas instance may ask of this machine over the link: the device decides, never the server."), }) export type Config = z.infer