{"_id":"@ayvazyan101/asterisk","_rev":"5-4028baafbaf093f64a0953f95d0c5ba1","name":"@ayvazyan101/asterisk","dist-tags":{"latest":"0.6.0"},"versions":{"0.4.0":{"name":"@ayvazyan101/asterisk","version":"0.4.0","keywords":["ai","agent","cli","ollama","anthropic","telegram-bot","local-first"],"license":"Apache-2.0","_id":"@ayvazyan101/asterisk@0.4.0","maintainers":[{"name":"ayvazyan10","email":"ayvazyan10@gmail.com"}],"homepage":"https://github.com/ayvazyan10/asterisk#readme","bugs":{"url":"https://github.com/ayvazyan10/asterisk/issues"},"os":["linux","darwin"],"bin":{"asterisk":"bin/asterisk"},"dist":{"shasum":"47c81b988bfe333d2a5adf8bf7523bdeb30aa2cb","tarball":"https://registry.npmjs.org/@ayvazyan101/asterisk/-/asterisk-0.4.0.tgz","fileCount":193,"integrity":"sha512-jec6i8QKo8vbi4OZvEoYcNfUAyR73Tau/6XkmYgIqgMYblz4HyeYpSutzBvGTf/3I90r1Xkc+0M6zRbNbIPtOg==","signatures":[{"sig":"MEUCIFgT+9yGBCy5eMF4jPNonLMcXMfB9NaZ2GyIAE3Mjo5YAiEA4e4sp9wVKU3fTJxBvhzFZYfwy9WLBI+WL6OLezzSjxI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ayvazyan101%2fasterisk@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":44248349},"type":"module","engines":{"bun":">=1.2.0"},"gitHead":"26dad5f88c6404d42f2a7f86c6747189aa86233f","private":false,"scripts":{"dev":"bun src/entrypoints/cli.tsx","lint":"biome check --error-on-warnings src/ tests/ scripts/","test":"vitest run","build":"bun scripts/build.ts","format":"biome format --write src/ tests/ scripts/","lint:fix":"biome check --write src/ tests/ scripts/","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"bun run build"},"_npmUser":{"name":"ayvazyan10","email":"ayvazyan10@gmail.com"},"repository":{"url":"git+https://github.com/ayvazyan10/asterisk.git","type":"git"},"_npmVersion":"10.9.8","description":"Asterisk — lightweight, personal AI assistant with Ollama-first model support, daemon controls, and a Telegram bot bridge.","directories":{},"_nodeVersion":"22.23.2","dependencies":{"ink":"^5.0.0","zod":"^3.24.0","pino":"^9.5.0","chalk":"^5.4.0","execa":"^9.5.0","react":"^18.3.0","grammy":"^1.30.0","undici":"^7.3.0","playwright":"^1.59.1","tinyglobby":"^0.2.16","ink-text-input":"^6.0.0","@anthropic-ai/sdk":"^0.30.0","@modelcontextprotocol/sdk":"^1.29.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"bun@1.2.0","devDependencies":{"vitest":"^4.0.0","@types/bun":"^1.3.13","typescript":"^5.7.0","@types/node":"^22.10.0","@types/react":"^18.3.0","@biomejs/biome":"^1.9.0","@vitest/coverage-v8":"^4.1.5"},"_npmOperationalInternal":{"tmp":"tmp/asterisk_0.4.0_1786566696909_0.04475915535563191","host":"s3://npm-registry-packages-npm-production"}},"0.4.1":{"name":"@ayvazyan101/asterisk","version":"0.4.1","keywords":["ai","agent","cli","ollama","anthropic","telegram-bot","local-first"],"license":"Apache-2.0","_id":"@ayvazyan101/asterisk@0.4.1","maintainers":[{"name":"ayvazyan10","email":"ayvazyan10@gmail.com"}],"homepage":"https://github.com/ayvazyan10/asterisk#readme","bugs":{"url":"https://github.com/ayvazyan10/asterisk/issues"},"os":["linux","darwin"],"bin":{"asterisk":"bin/asterisk"},"dist":{"shasum":"faff68843262ef13b25ac3904520226c3308f8a7","tarball":"https://registry.npmjs.org/@ayvazyan101/asterisk/-/asterisk-0.4.1.tgz","fileCount":193,"integrity":"sha512-o/Kn6Pt1ZLBygT8XE/waEj7iFRjxUmR7dII1Wx555pjelF3SL9TcfvCv9YhdI0MmGOw35oicQZpsv9mvvyZq1g==","signatures":[{"sig":"MEUCIDDVa8rTNY38dk7LQeFO52X4eSFkCXdtW9jjECfkPr+xAiEA+3AaJF9nDn1dwV4DfeXiiAB8M3ggUn334gzKJrpdTuc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ayvazyan101%2fasterisk@0.4.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":20170524},"type":"module","engines":{"bun":">=1.2.0"},"gitHead":"964ee4ce93870f3e6deb7384447aa1563a4074c1","private":false,"scripts":{"dev":"bun src/entrypoints/cli.tsx","lint":"biome check --error-on-warnings src/ tests/ scripts/","test":"vitest run","build":"bun scripts/build.ts","format":"biome format --write src/ tests/ scripts/","lint:fix":"biome check --write src/ tests/ scripts/","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"bun run build --minify"},"_npmUser":{"name":"ayvazyan10","email":"ayvazyan10@gmail.com"},"repository":{"url":"git+https://github.com/ayvazyan10/asterisk.git","type":"git"},"_npmVersion":"10.9.8","description":"Asterisk — lightweight, personal AI assistant with Ollama-first model support, daemon controls, and a Telegram bot bridge.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"ink":"^5.0.0","zod":"^3.24.0","pino":"^9.5.0","chalk":"^5.4.0","execa":"^9.5.0","react":"^18.3.0","grammy":"^1.30.0","undici":"^7.3.0","playwright":"^1.59.1","tinyglobby":"^0.2.16","ink-text-input":"^6.0.0","@anthropic-ai/sdk":"^0.30.0","@modelcontextprotocol/sdk":"^1.29.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"bun@1.2.0","devDependencies":{"vitest":"^4.0.0","@types/bun":"^1.3.13","typescript":"^5.7.0","@types/node":"^22.10.0","@types/react":"^18.3.0","@biomejs/biome":"^1.9.0","@vitest/coverage-v8":"^4.1.5"},"_npmOperationalInternal":{"tmp":"tmp/asterisk_0.4.1_1786567887128_0.10862288515739915","host":"s3://npm-registry-packages-npm-production"}},"0.4.2":{"name":"@ayvazyan101/asterisk","version":"0.4.2","keywords":["ai","agent","cli","llama.cpp","anthropic","telegram-bot","local-first"],"license":"Apache-2.0","_id":"@ayvazyan101/asterisk@0.4.2","maintainers":[{"name":"ayvazyan10","email":"ayvazyan10@gmail.com"}],"homepage":"https://github.com/ayvazyan10/asterisk#readme","bugs":{"url":"https://github.com/ayvazyan10/asterisk/issues"},"os":["linux","darwin"],"bin":{"asterisk":"bin/asterisk"},"dist":{"shasum":"52d0917e349c02ac30e43dcca64598b9425ee9dd","tarball":"https://registry.npmjs.org/@ayvazyan101/asterisk/-/asterisk-0.4.2.tgz","fileCount":220,"integrity":"sha512-csHYWAHReRwmonrWAEZ6DGj9WH5g+PHW/LcyHzmfmX3vQyU/GFw/3akjwjDTJnmhpk5c/qIsVuUMUOUoj5tK5A==","signatures":[{"sig":"MEUCIDVupfNkwn6v+4JG9+vGxMet+bLS3A+ogvsZz0bzI9VvAiEArej4JVMb/FcmIR8sjKeqCSgVhjG3bAkgcHbXlsP+ZCE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ayvazyan101%2fasterisk@0.4.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":20945997},"type":"module","engines":{"bun":">=1.2.0"},"gitHead":"2887e3baff0d983cc2a8bdbd2666eab42721510e","private":false,"scripts":{"dev":"bun src/entrypoints/cli.tsx","lint":"biome check --error-on-warnings src/ tests/ scripts/","test":"vitest run","build":"bun scripts/build.ts","format":"biome format --write src/ tests/ scripts/","lint:fix":"biome check --write src/ tests/ scripts/","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"bun run build --minify"},"_npmUser":{"name":"ayvazyan10","email":"ayvazyan10@gmail.com"},"repository":{"url":"git+https://github.com/ayvazyan10/asterisk.git","type":"git"},"_npmVersion":"10.9.8","description":"Asterisk — lightweight, personal AI assistant with local-first model support (llama.cpp and any OpenAI-compatible server), daemon controls, and a Telegram bot bridge.","directories":{},"_nodeVersion":"22.23.2","dependencies":{"ink":"^5.0.0","zod":"^3.24.0","pino":"^9.5.0","chalk":"^5.4.0","execa":"^9.5.0","react":"^18.3.0","grammy":"^1.30.0","undici":"^7.3.0","playwright":"^1.59.1","tinyglobby":"^0.2.16","ink-text-input":"^6.0.0","@anthropic-ai/sdk":"^0.30.0","@modelcontextprotocol/sdk":"^1.29.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"bun@1.2.0","devDependencies":{"vitest":"^4.0.0","@types/bun":"^1.3.13","typescript":"^5.7.0","@types/node":"^22.10.0","@types/react":"^18.3.0","@biomejs/biome":"^1.9.0","@vitest/coverage-v8":"^4.1.5"},"_npmOperationalInternal":{"tmp":"tmp/asterisk_0.4.2_1787815209545_0.6827685822310083","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@ayvazyan101/asterisk","version":"0.5.0","keywords":["ai","agent","cli","llama.cpp","anthropic","telegram-bot","local-first"],"license":"Apache-2.0","_id":"@ayvazyan101/asterisk@0.5.0","maintainers":[{"name":"ayvazyan10","email":"ayvazyan10@gmail.com"}],"homepage":"https://github.com/ayvazyan10/asterisk#readme","bugs":{"url":"https://github.com/ayvazyan10/asterisk/issues"},"os":["linux","darwin"],"bin":{"asterisk":"bin/asterisk"},"dist":{"shasum":"39f74d2e1e34828d06eaf6f989804951f1dab582","tarball":"https://registry.npmjs.org/@ayvazyan101/asterisk/-/asterisk-0.5.0.tgz","fileCount":253,"integrity":"sha512-V8R/u7pQtb5gTYpp/8FDYgIcJCLLg8E1qKIYHrQ2tUZ2AYg1xQxTXtD5wrkf+ptZ7Lgtr30vrZwAD8qAM0gp6A==","signatures":[{"sig":"MEQCIBgNd7jvm6c7Zq5Srw5nTfYq1MSKg8DjTF00hdFALF9bAiAh07L+FVWmts/kIF6gXYtvaFqL0cdbCjfbh32gGi7vnQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ayvazyan101%2fasterisk@0.5.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":7306346},"type":"module","engines":{"bun":">=1.2.0"},"gitHead":"be9179990cbbbc91abe540adae7a01ba944b6ab2","private":false,"scripts":{"dev":"bun src/entrypoints/cli.tsx","lint":"biome check --error-on-warnings src/ tests/ scripts/","test":"vitest run","build":"bun scripts/build.ts","format":"biome format --write src/ tests/ scripts/","lint:fix":"biome check --write src/ tests/ scripts/","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"bun run build --minify"},"_npmUser":{"name":"ayvazyan10","email":"ayvazyan10@gmail.com"},"repository":{"url":"git+https://github.com/ayvazyan10/asterisk.git","type":"git"},"_npmVersion":"10.9.8","description":"Asterisk — lightweight, personal AI assistant with local-first model support (llama.cpp and any OpenAI-compatible server), daemon controls, and a Telegram bot bridge.","directories":{},"_nodeVersion":"22.23.2","dependencies":{"ink":"^5.0.0","zod":"^3.24.0","pino":"^9.5.0","chalk":"^5.4.0","execa":"^9.5.0","react":"^18.3.0","grammy":"^1.30.0","undici":"^7.3.0","tinyglobby":"^0.2.16","ink-text-input":"^6.0.0","@anthropic-ai/sdk":"^0.122.0","@modelcontextprotocol/sdk":"^1.29.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"bun@1.2.0","devDependencies":{"vitest":"^4.0.0","@types/bun":"^1.3.13","playwright":"^1.62.1","typescript":"^5.7.0","@types/node":"^22.10.0","@types/react":"^18.3.0","@biomejs/biome":"^1.9.0","@vitest/coverage-v8":"^4.1.5"},"peerDependencies":{"playwright":"^1.59.1"},"peerDependenciesMeta":{"playwright":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/asterisk_0.5.0_1787953331609_0.2827534047530289","host":"s3://npm-registry-packages-npm-production"}},"0.6.0":{"name":"@ayvazyan101/asterisk","version":"0.6.0","description":"Asterisk — lightweight, personal AI assistant with local-first model support (llama.cpp and any OpenAI-compatible server), daemon controls, and a Telegram bot bridge.","license":"Apache-2.0","type":"module","private":false,"bin":{"asterisk":"bin/asterisk"},"repository":{"type":"git","url":"git+https://github.com/ayvazyan10/asterisk.git"},"homepage":"https://github.com/ayvazyan10/asterisk#readme","bugs":{"url":"https://github.com/ayvazyan10/asterisk/issues"},"publishConfig":{"access":"public"},"os":["linux","darwin"],"scripts":{"build":"bun scripts/build.ts","typecheck":"tsc --noEmit","lint":"biome check --error-on-warnings src/ tests/ scripts/","lint:fix":"biome check --write src/ tests/ scripts/","format":"biome format --write src/ tests/ scripts/","test":"vitest run","test:watch":"vitest","dev":"bun src/entrypoints/cli.tsx","test:coverage":"vitest run --coverage","prepublishOnly":"bun run build --minify"},"dependencies":{"@anthropic-ai/sdk":"^0.122.0","@modelcontextprotocol/sdk":"^1.29.0","chalk":"^5.4.0","execa":"^9.5.0","grammy":"^1.30.0","ink":"^5.0.0","ink-text-input":"^6.0.0","pino":"^9.5.0","react":"^18.3.0","tinyglobby":"^0.2.16","undici":"^7.3.0","zod":"^3.24.0"},"peerDependencies":{"playwright":"^1.59.1"},"peerDependenciesMeta":{"playwright":{"optional":true}},"devDependencies":{"@biomejs/biome":"^1.9.0","@types/bun":"^1.3.13","@types/node":"^22.10.0","@types/react":"^18.3.0","@vitest/coverage-v8":"^4.1.5","playwright":"^1.62.1","typescript":"^5.7.0","vitest":"^4.0.0"},"engines":{"bun":">=1.2.0"},"packageManager":"bun@1.2.0","keywords":["ai","agent","cli","llama.cpp","anthropic","telegram-bot","local-first"],"_id":"@ayvazyan101/asterisk@0.6.0","gitHead":"0467051b205b2f42120a28fcd65595273b12c2bf","_nodeVersion":"22.23.2","_npmVersion":"10.9.8","dist":{"integrity":"sha512-AUjQhKSs4pTzMEYFZQ9I7fxp6H3RAR6BBzvsa3GEbxUcNPkCxFoGa5rTHJtS8vtgrW5WAYBbYNce5PuTMA5YUg==","shasum":"5dd42910dee8d5efea66d178fe7e1f6cda30f51f","tarball":"https://registry.npmjs.org/@ayvazyan101/asterisk/-/asterisk-0.6.0.tgz","fileCount":262,"unpackedSize":7386154,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ayvazyan101%2fasterisk@0.6.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCotz7EOcDnThCnHpSUImiz19/VgUEckW27aKUlMcAHXgIhAM+80cYM5DZhTHIU2WKMjpAIhPBptNJbehSGTPWBWcHY"}]},"_npmUser":{"name":"ayvazyan10","email":"ayvazyan10@gmail.com"},"directories":{},"maintainers":[{"name":"ayvazyan10","email":"ayvazyan10@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/asterisk_0.6.0_1788178057281_0.5931196760511217"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-12T20:31:36.652Z","modified":"2026-08-31T12:07:37.826Z","0.4.0":"2026-08-12T20:31:37.259Z","0.4.1":"2026-08-12T20:51:27.607Z","0.4.2":"2026-08-27T07:20:09.807Z","0.5.0":"2026-08-28T21:42:11.799Z","0.6.0":"2026-08-31T12:07:37.475Z"},"bugs":{"url":"https://github.com/ayvazyan10/asterisk/issues"},"license":"Apache-2.0","homepage":"https://github.com/ayvazyan10/asterisk#readme","keywords":["ai","agent","cli","llama.cpp","anthropic","telegram-bot","local-first"],"repository":{"type":"git","url":"git+https://github.com/ayvazyan10/asterisk.git"},"description":"Asterisk — lightweight, personal AI assistant with local-first model support (llama.cpp and any OpenAI-compatible server), daemon controls, and a Telegram bot bridge.","maintainers":[{"name":"ayvazyan10","email":"ayvazyan10@gmail.com"}],"readme":"# Asterisk\n\nA lightweight, personal AI assistant. Asterisk gives you an interactive\nagent in your terminal and an optional long-running daemon that bridges the\nsame assistant to Telegram.\n\n- **Local by default** — talks to a local [llama.cpp](https://github.com/ggml-org/llama.cpp)\n  server (or LM Studio, vLLM, Jan, or any `/v1/chat/completions` endpoint) out\n  of the box, and asks it which model it is serving rather than making you\n  configure one; the public `@anthropic-ai/sdk` is wired in as an opt-in\n  alternative.\n- **Real tools** — filesystem, shell, web, **a real Chromium browser via\n  Playwright**, MCP-server integration, sub-agents, scheduled and recurring\n  prompts, and more.\n- **No telemetry, no cloud control plane.** Everything runs on your machine.\n- **Built on documented APIs** — Anthropic Messages + tool-use loop, the\n  OpenAI-compatible chat API, Telegram Bot API ([grammY](https://grammy.dev)),\n  [Model Context Protocol](https://modelcontextprotocol.io), Playwright.\n- **Apache 2.0** licensed.\n\nStatus `0.4.2` — early but real. 46 built-in tools, 28 slash commands,\n14 daemon-managed scheduling/lifecycle features, **29 bundled skills**,\n**27 specialised sub-agent types** the agent can dispatch on demand,\nlayered multi-language rules, switchable output styles\n(default / concise / explanatory / learning), a SOUL.md persona\nsystem that bot users can manage per-chat, and an agent loop\nhardened with tool concurrency, context compaction, prompt caching,\na Bash permission boundary, file history, and conversation persistence.\n\n## Install\n\nOne-line install (macOS / Linux / WSL):\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/ayvazyan10/asterisk/master/install.sh | bash\n```\n\nThe installer:\n\n1. Installs [Bun](https://bun.sh) ≥ 1.2 if it isn't already on your machine.\n2. Clones Asterisk into `~/.local/share/asterisk`.\n3. Builds `dist/`.\n4. Downloads Chromium for Playwright (~150 MB; skip with `ASTERISK_SKIP_BROWSERS=1`).\n5. Symlinks `~/.local/bin/asterisk` so the `asterisk` command is on your PATH.\n\nOverride locations or branch via env vars on the receiving `bash`:\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/ayvazyan10/asterisk/master/install.sh \\\n  | ASTERISK_INSTALL_DIR=/opt/asterisk ASTERISK_BIN_DIR=/usr/local/bin bash\n```\n\nAvailable: `ASTERISK_INSTALL_DIR` (default `~/.local/share/asterisk`),\n`ASTERISK_BIN_DIR` (default `~/.local/bin`), `ASTERISK_BRANCH` (default\n`master`), `ASTERISK_REPO_URL`, `ASTERISK_SKIP_BROWSERS`.\n\nTo uninstall:\n\n```bash\nbash <(curl -fsSL https://raw.githubusercontent.com/ayvazyan10/asterisk/master/uninstall.sh)\n```\n\nYour `~/.asterisk/` config is preserved unless you delete it explicitly.\n\n### From npm\n\n```bash\nnpm install -g @ayvazyan101/asterisk\n```\n\nPublished under a scope because the bare `asterisk` name on npm belongs to an\nunrelated package. Releases are published from CI with npm provenance, so the\nregistry carries a signed attestation tying each tarball to the workflow run\nand commit that built it. **Bun must already be installed** — the bundles target the\nBun runtime, and npm will not bring it along; the `asterisk` command stops with\nan install hint if it can't find Bun. Browser tools also need a one-off\n`bun playwright install chromium`. The one-line installer above handles both\nfor you, which is why it stays the recommended path.\n\nmacOS and Linux only, including WSL. The `asterisk` command is a bash\ndispatcher, so Windows without WSL is not supported.\n\n### From source\n\n```bash\ngit clone https://github.com/ayvazyan10/asterisk.git && cd asterisk\nbun install\nbun playwright install chromium    # optional — only if you want browser tools\nbun run build\n./bin/asterisk help\n```\n\n## Quick start\n\nRequirements:\n\n- [Bun](https://bun.sh) ≥ 1.2 (handled by the installer).\n- A local model server speaking the OpenAI `/v1/chat/completions` API —\n  llama.cpp's `llama-server`, LM Studio, vLLM, Jan, LocalAI, or Ollama's own\n  OpenAI-compatible endpoint — OR an `ANTHROPIC_API_KEY`.\n\n### Connecting a local model\n\nPoint Asterisk at the endpoint; the model name is optional, because Asterisk\nasks the server what it is serving:\n\n```bash\nasterisk configure       # base URL; leave the model blank to auto-detect\n# or, in the REPL:\n/model                   # pick from what the server lists, or \"auto\"\n```\n\nA llama.cpp server started with `--alias gemma-4-26b --port 8080` is reached\nat `http://127.0.0.1:8080/v1`. Tool calling, streaming, and reasoning output\n(`--reasoning-format deepseek`) are all supported. Set\n`ASTERISK_OPENAI_API_KEY` only if the endpoint is a hosted service that needs\none.\n\n**The active model is detected, not configured.** Before each request Asterisk\nasks `GET /v1/models` which model the server is holding, and uses that — so\nswapping the model means restarting your server, with nothing to change on\nAsterisk's side. The answer is cached for a minute, so this costs one request\nper minute, not one per turn.\n\nThe same listing carries `meta.n_ctx`, the context window the server was\nactually started with, and compaction budgets history against it. That number\nused to be a guess: 128k assumed by default, which wastes more than half a\n262 144-token window and overflows an 8 192-token one before compaction ever\nfires.\n\nPin a model with `/model <id>` when one endpoint serves several; `/model auto`\ngoes back to detection. If the server cannot be reached, a pinned name is used\nas the fallback, and with neither the failure names both halves of the fix.\n\n```bash\nasterisk                # interactive REPL\nasterisk start          # daemon mode (Telegram bridge)\nasterisk status         # daemon pid + log size\nasterisk logs 100\nasterisk restart\nasterisk stop\nasterisk configure      # interactive wizard for provider + bots + MCP\nasterisk web            # web control panel for every setting (background)\nasterisk web stop       # stop the panel and free its port\nasterisk help\n```\n\n## Web control panel\n\n`asterisk web` serves a settings UI at `http://127.0.0.1:4321`. It is the\nwhole configuration surface in one place:\n\n- **Settings** — every field Asterisk understands, with its validation\n  bounds and help text. The form is generated from the configuration\n  schema, so it is never out of date with the code.\n- **Secrets** — API keys and bot tokens. Values are write-only: the browser\n  only ever receives a masked fingerprint.\n- **MCP servers** and **hooks** — add, edit, enable, delete.\n- **Rules & skills** — a markdown editor for your rules, skills, sub-agent\n  definitions and persona files.\n- **Diagnostics, daemon control, log tail, audit trail** — the same ground\n  as `/doctor`, `asterisk start|stop`, and `asterisk logs`.\n\n```bash\nasterisk web                     # starts in the background, prints the link\nasterisk web stop                # stops it and frees the port\nasterisk web --port 8080\nasterisk web --foreground        # run in this terminal instead (systemd, Docker)\nasterisk web --print-token       # issue another token\nasterisk web --no-auth           # loopback binds only\n```\n\n`asterisk web` returns the terminal immediately: the server runs as a detached\nchild with its own pid file (`~/.asterisk/web.pid`) and log\n(`~/.asterisk/logs/web.log`), and `asterisk web stop` terminates it and releases\nthe port. Its lifecycle is independent of the daemon's — `asterisk stop` leaves\nthe panel running, and `asterisk web stop` leaves the bots running.\n\nA token is required by default and is exchanged for an httpOnly session\ncookie on first load. Only SHA-256 hashes are stored, so a lost token is\nregenerated rather than recovered. Binding to a non-loopback address without\nauthentication is refused outright.\n\n## REPL highlights\n\n- Type `/` and a filtered command picker pops up — `↑↓` navigate, `Tab`\n  completes, `Enter` runs, `Esc` clears.\n- Slash commands open **forms** for fields and **list pickers** for choices,\n  not CLI args. Ask the agent to add an MCP server and it walks you through\n  a transport-pick → form flow.\n- Long tool output is collapsed by default with `[+N more lines · Ctrl+O to expand]`.\n- The thinking indicator sits **above** the input, not inside it — you can\n  type a side question while the agent works; it gets queued and runs after\n  the current turn.\n- Markdown rendered: `**bold**`, `*italic*`, `` `code` ``, fenced code blocks,\n  bullets, headers, blockquotes — all properly indented under the cyan\n  assistant marker.\n- Screenshots render inline on iTerm2 / WezTerm / Kitty; on other terminals\n  the `file://` URL is clickable and `open: true` launches the OS viewer.\n\n## Slash commands\n\n| Command            | What it does                                                |\n| ------------------ | ----------------------------------------------------------- |\n| `/help [name]`     | List commands or show details for one                       |\n| `/clear`           | Forget the current conversation history                     |\n| `/model [name]`    | List installed models or switch the active one              |\n| `/provider [name]` | Switch between `openai-compatible` and `anthropic`         |\n| `/tools`           | List registered tools (built-ins + MCP)                     |\n| `/status`          | Live runtime view: provider, bots, MCP, daemon              |\n| `/config`          | Interactive forms for each config section                   |\n| `/reset`           | Clear history and rebuild the provider from config          |\n| `/mcp`             | Manage MCP servers — list/add/edit/remove/reload (visual)   |\n| `/agents`          | List specialised sub-agent types you can dispatch           |\n| `/output-style`    | Switch reply style — default / concise / explanatory / learning |\n| `/rules`           | List the rules currently loaded into the system prompt      |\n| `/skills`          | List installed skills; `validate` reports broken ones        |\n| `/skill [name]`    | Run a skill — picker if no name given                       |\n| `/soul [verb]`     | Show / `init` / `where` your SOUL.md persona                |\n| `/plan`            | Toggle Plan Mode (read-only research mode)                  |\n| `/tasks`           | List the agent's in-flight tasks for this session           |\n| `/hooks`           | Manage agent-loop lifecycle hooks (visual)                  |\n| `/permissions`     | Inspect and edit what `Bash` may run without asking         |\n| `/doctor`          | Diagnostics — local model, Anthropic, system tools, MCP     |\n| `/sessions`        | List saved conversations                                    |\n| `/resume`          | Resume a saved conversation                                 |\n| `/forget`          | Delete a saved conversation                                 |\n| `/diff`            | Show a structured git diff summary                          |\n| `/review`          | Review current git changes for risk patterns                |\n| `/code`            | Code intelligence — symbols, definitions, references        |\n| `/update`          | Check for updates or self-update to the latest version      |\n| `/quit`            | Exit the REPL                                               |\n\n## Built-in tools\n\nThe agent has these tools out of the box:\n\n**Filesystem & shell**\n`Bash` · `Read` · `Write` · `Edit` · `Grep` · `Glob`\n\nThe agent can **see** the screenshots it takes: `BrowserScreenshot` feeds the\nimage back through the model's vision input, capped and evicted from history\nby the `vision` settings. Turn it off for a text-only model.\n\n**Browser (real Chromium via Playwright)**\n`BrowserNavigate` · `BrowserClick` · `BrowserType` · `BrowserPress` ·\n`BrowserSnapshot` · `BrowserScreenshot` · `BrowserWait` · `BrowserClose`\n\n**Web research**\n`WebFetch` (URL → readable text) · `WebSearch` (Brave / Tavily / SearXNG /\nDDG instant-answer, picks the first backend you've configured a key for)\n\n**Speech**\n`Transcribe` — an audio file to text, through the same backends that read\nincoming voice messages. See [Voice messages](#voice-messages).\n\n**Memory**\n`Remember` · `Recall` — notes that survive across sessions, searched with\nSQLite FTS5 (falling back to substring search on a build without it).\n\n**Planning**\n`TaskCreate` · `TaskUpdate` · `TaskList` · `TaskGet` · `TaskStop` — the\nagent's own todo list, used to track multi-step work.\n\n**Plan Mode**\n`EnterPlanMode` / `ExitPlanMode` — toggle a flag that hides write tools so\nthe agent can only research.\n\n**Delegation**\n`Agent` — spawn a sub-agent in an isolated conversation for focused\nresearch or parallel investigation. Pass `subagent_type: <name>` to\ndispatch a specialised role (code-reviewer, security-reviewer, planner,\nexplore, …) — see [Sub-agent types](#sub-agent-types) below.\n\n**Worktree**\n`EnterWorktree` / `ExitWorktree` — create / remove a `git worktree` for\nrisky changes that shouldn't touch the active branch.\n\n**Monitoring & notifications**\n`Monitor` (start/tail/stop background commands) · `PushNotification`\n(webhook out-of-band) · `RemoteTrigger` (generic HTTP request).\n\n**Interactive**\n`AskUserQuestion` — pause the loop and ask the user a question with\nfree-text or a list-picker; user's answer resolves the tool.\n\n**Scheduling** (daemon-managed)\n`ScheduleWakeup` (one-shot delay) · `CronCreate` / `CronDelete` / `CronList`\n(5-field cron expressions). The daemon polls every 30s and dispatches due\nitems as fresh agent turns.\n\n**Batch**\n`RunCode` — run a short program that calls the tools above in a loop, in one\nturn instead of N. Bash can already loop; what it cannot do is call `Edit`,\n`Grep`, `WebFetch` or `Remember`, so a batch whose loop body is an Asterisk\ntool used to be one turn per item.\n\n```js\nconst found = tool('Grep', { pattern: 'oldName', path: 'src' });\nlet done = 0;\nfor (const line of found.output.split('\\n')) {\n  const path = line.split(':')[0];\n  if (!path) continue;\n  const r = tool('Edit', { path, oldString: 'oldName', newString: 'newName', replaceAll: true });\n  if (r.ok) done += 1; else log(`failed ${path}: ${r.output}`);\n}\nreturn done;\n```\n\nThe language is a **subset of JavaScript**, not JavaScript, and it is not run\nby `node:vm` — a vm context handed a single host callable is not a boundary\n(`callTool.constructor(…)` reaches the host realm's `Function`, and from\nthere `process.env` and `fs.writeFileSync`). A program is parsed to an AST and\nwalked by an interpreter with no host object graph to reach, which is what\nlets tool calls keep their own rules: `Bash` from a program still asks you to\napprove the command, `Write` and `Edit` still refuse paths outside the\nwritable set, because it is the same call. There is no `function`, `class`,\n`new`, `import`, `eval`, `try`/`catch` or regex; anything outside the subset\nis a syntax error naming what to write instead. Bounded on wall clock, tool\ncalls, interpreter steps, call depth and value size, so `while (true) {}` ends\nthe tool call rather than the session. `RunCode` cannot call itself, `Agent`\nor `AskUserQuestion`.\n\n**Tool discovery**\n`ToolSearch` — keyword search across the registered tools, returning full tool\ndefinitions. Tool schemas are deferred by default (`tools.deferSchemas`): the\nrequest carries the built-ins plus a one-line pointer at the connected MCP\nservers, and `ToolSearch` loads the rest on demand — a loaded tool stays\navailable for the rest of the conversation. On a three-server install that is\n~207 KB of schema per request down to ~25 KB. Set `tools.deferSchemas` to\n`all` to defer the rarely used built-ins too, or `off` to send everything.\n\nPlus any tools exposed by configured MCP servers, namespaced as\n`<servername>__<toolname>`.\n\n## Skills, rules, hooks, souls\n\n**Skills** — reusable workflows. 29 bundled out of the box:\n\n*Core workflow:*\n- `simplify` — review your recent changes for reuse / quality / efficiency\n- `batch` — apply one operation across many targets, with progress tracking\n- `stuck` — diagnose why a task is blocked, propose alternatives\n- `dream` — free-form roam, find one improvement worth making\n- `skillify` — capture the current conversation as a new SKILL.md\n- `verify` — run typecheck / lint / tests / build, classify each result,\n  isolate root causes for any failures\n- `debug` — diagnose a specific failure end-to-end: reproduce, read the\n  error literally, hypothesise + verify, propose a concrete fix, re-run\n- `feature` — drive a feature plan → implement → review → verify →\n  commit, with Plan Mode discipline on the planning phase\n\n*PRP pipeline (granular alternative to `feature`):*\n- `prp-plan` — write a one-page Plan-Requirements-Pitch doc in Plan Mode\n- `prp-implement` — execute against the PRP doc, with task tracking + verify\n- `prp-pr` — open a real GitHub PR with summary + test plan via `gh`\n- `prp-commit` — write a coherent commit with a real WHY message\n\n*Loops + scheduling:*\n- `loop` — recurring task with explicit stop conditions\n- `schedule` — friendly wrapper over `ScheduleWakeup` / `CronCreate`\n- `santa-loop` — adversarial dual-review: dispatches `code-reviewer`\n  and `security-reviewer` sub-agents in parallel, iterates until both\n  approve or hits a 5-round cap\n\n*Quality / security tooling:*\n- `dep-audit` — `npm audit` / `cargo audit` / `pip-audit` / `govulncheck`,\n  classify by severity, propose upgrades\n- `security-scan` — active scanning (gitleaks for secrets, trivy / gosec /\n  bandit / semgrep / tfsec depending on stack)\n- `cloud-infrastructure-security` — IaC review for Terraform / Pulumi /\n  CDK / Helm / K8s / CloudFormation: IAM wildcards, exposed ports,\n  plaintext secrets, supply-chain\n- `pr-review` — review an open GitHub PR end-to-end via \\`gh\\`\n\n*LLM / agent ops:*\n- `ai-regression-testing` — golden-trace harness for LLM outputs\n- `eval-harness` — score outputs against a rubric (graded eval)\n- `prompt-optimizer` — iterate on a prompt with measurable lift\n- `mcp-server-patterns` — build an MCP server with the public SDK\n- `data-scraper-agent` — robust scraper using `BrowserNavigate` + `Snapshot`\n- `regex-vs-llm-structured-text` — tactical guide on when to reach for\n  each (and the hybrid pre-filter pattern)\n\n*Auditing your setup:*\n- `audit-memory` — inventory rules / souls / hooks; flag stale entries\n- `skill-stocktake` — inventory user-installed skills + agents; surface\n  dead weight\n\n*Other:*\n- `release-notes` — generate notes from `git log <prev>..HEAD`, grouped\n  by type, with breaking changes promoted\n- `youtube-summarizer` — summarise a YouTube video (uses `yt-dlp` if\n  available for the transcript, falls back to WebFetch on description)\n\nAdd your own at `~/.asterisk/skills/<name>/SKILL.md` (user-global) or\n`<repo>/.asterisk/skills/<name>/SKILL.md` (project-local). User/project\nskills override bundled ones with the same name.\n\n**Rules** — markdown auto-loaded into the system prompt. Two layouts:\n\n*Flat (simple):*\n- `~/.asterisk/rules/*.md` — user-global (e.g. tone, coding style)\n- `<repo>/.asterisk/rules/*.md` — project-local\n- `<repo>/ASTERISK.md` — project root marker\n\n*Layered (multi-language):*\n- `~/.asterisk/rules/common/*.md` — universal, always loaded\n- `~/.asterisk/rules/<lang>/*.md` — only loaded when the project's\n  primary language matches (`typescript`, `python`, `golang`, `rust`,\n  `java`, `php`, `swift`, `dart`, `cpp`, `web`, `ruby`, …).\n- Same structure under `<repo>/.asterisk/rules/`.\n- Auto-detection from manifest files (`package.json`, `Cargo.toml`,\n  `pyproject.toml`, `go.mod`, …). Override via `ASTERISK_LANG=python`.\n  This names the *project's* language, and is separate from\n  `ASTERISK_LOCALE`, which names the language you read.\n\n**Output styles** — pluggable behaviour modifiers spliced into the\nsystem prompt alongside rules + soul. Switch via `/output-style <name>`\nin the REPL or `/style <name>` in the bots; persists to `config.json`.\nFour bundled:\n\n- `default` — baseline, no extra style instructions.\n- `concise` — trim every reply to the minimum useful answer; lists\n  over prose; skip preambles and pleasantries.\n- `explanatory` — show reasoning + tradeoffs alongside the answer.\n  Good for learning a codebase or onboarding to a domain.\n- `learning` — collaborative; the agent surfaces non-trivial design\n  decisions via `AskUserQuestion` and waits for the user to pick before\n  applying.\n\n**Hooks** — shell commands fired at agent-loop lifecycle events\n(`before_turn`, `after_turn`, `before_tool`, `after_tool`, `on_error`).\nConfigured via `/hooks` (visual). The hook command receives the event\npayload as JSON on stdin; stdout is surfaced as a system note in the\ntranscript.\n\n**Souls** — `SOUL.md` describes who the assistant should be and who it's\ntalking to. Spliced into the system prompt before rules. Three layers,\nall optional, later wins on conflict:\n\n- `~/.asterisk/SOUL.md` — operator persona (applies everywhere)\n- `~/.asterisk/souls/<scope>-<sid>.md` — **per-chat** persona; written by\n  Telegram users via `/soul set <text>`, so each chat owns its\n  own description without affecting anyone else\n- `<repo>/.asterisk/SOUL.md` or `<repo>/SOUL.md` — project-local persona\n\nIn the REPL: `/soul` shows what's loaded, `/soul init` drops a starter\ntemplate at `~/.asterisk/SOUL.md`, `/soul where` lists the search paths.\n\nIn Telegram: `/soul`, `/soul set <multi-line markdown>`,\n`/soul edit`, `/soul clear`, `/soul help` — all scoped to the current\nchat. `/soul set Call me Levon, reply in Russian, skip apologies` is\nenough to teach the bot a new persona for that chat alone.\n\n## Sub-agent types\n\nThe `Agent` tool can dispatch a sub-agent with a tailored system prompt\nand (sometimes) a restricted tool-set. The parent agent passes\n`subagent_type: <name>` to pick a specialist; omitting it spawns a\ngeneral-purpose sub-agent with the parent's full tools.\n\n**27 bundled types out of the box:**\n\n- **Exploration / research:** `general-purpose`, `explore` (read-only\n  scout), `docs-lookup`\n- **Planning / architecture:** `planner`, `architect`\n- **Code review:** `code-reviewer`, `security-reviewer`,\n  `database-reviewer`, `performance-optimizer`, `refactor-cleaner`,\n  `doc-updater`\n- **Language-specific reviewers:** `typescript-reviewer`,\n  `python-reviewer`, `go-reviewer`, `rust-reviewer`\n- **Build / test:** `build-error-resolver`, `tdd-guide`, `e2e-runner`\n- **Domain:** `chief-of-staff` (multi-channel triage),\n  `healthcare-reviewer`\n- **Open-source pipeline:** `opensource-forker` →\n  `opensource-sanitizer` → `opensource-packager`\n- **Loops / harnesses:** `loop-operator`, `gan-planner`,\n  `gan-generator`, `gan-evaluator`\n\nAdd your own at `~/.asterisk/agents/<name>.md` (user-global) or\n`<repo>/.asterisk/agents/<name>.md` (project-local). Same markdown +\nfrontmatter format as skills:\n\n```markdown\n---\nname: my-reviewer\ndescription: Reviews against our internal style guide.\nallowedTools: Read, Grep, Glob, Bash\nmaxTurns: 12\n---\nYou review code against the patterns in our internal style guide …\n```\n\nUser/project files override bundled by name. Run `/agents` to see what's\nloaded.\n\n## Interface language\n\nThe REPL speaks English and Russian. The locale comes from the environment\nrather than from configuration, because it is a property of the terminal you\nare sitting at, not of the install:\n\n```bash\nASTERISK_LOCALE=ru asterisk      # explicit, wins over everything\nLANG=ru_RU.UTF-8 asterisk        # picked up automatically (LC_ALL first)\n```\n\n`ASTERISK_LANG=ru` also still selects the locale, for one more release, and\nwarns when it does. It was a poor name for two jobs: the rules loader reads\nthe same variable to pin the *project's* language (`typescript`, `python`),\nso setting it to `ru` for a Russian interface used to silently switch the\nper-language rule layer off. Use `ASTERISK_LOCALE` for what you read and\n`ASTERISK_LANG` for what the project is written in.\n\n**Only what you read is translated.** The system prompt, tool names, tool\ndescriptions and tool results stay English in every locale, and that is a\ncorrectness boundary rather than unfinished work: models are tuned on English\ntool descriptions, `Bash` is an identifier the provider matches on, and\ntranslating them would change how the agent behaves rather than how it looks.\nA key a translation is missing falls back to English instead of showing you\nthe key. Adding a language is `src/i18n/messages.ts` — the English catalogue\nis the type, so a translation cannot invent a key that does not exist.\n\n## Bot transports\n\n| Transport            | Status                | Notes                                              |\n| -------------------- | --------------------- | -------------------------------------------------- |\n| Telegram (grammY)    | Supported             | Bot token from @BotFather; allowlist required.     |\n\nTelegram is the only transport. WhatsApp support was removed in `0.4.0`:\nthe Meta Cloud path needed a Business Manager account most users of a\npersonal assistant will never have, and the web-js path drove WhatsApp Web\nthrough Puppeteer in violation of WhatsApp's Terms of Service — shipping a\nToS violation as a documented feature was the wrong default, however\nprominent the warning. The adapter contract in `src/bots/adapter.ts` is\nunchanged, so a new transport is still a self-contained module.\n\nThe bot is gated by config — it does not run unless you explicitly\nenable it via `asterisk configure`. It can also send media: any\nattachment the agent emits via the `Attach` tool (image, video, audio,\ndocument) is delivered as a real Telegram media message.\n\n**Telegram reply modes** (`bots.telegram.streamMode`):\n\n- `final` *(default)* — one message at the end of the turn. Cheapest, no\n  edit churn, identical to a typical chat reply.\n- `status` — sends a `◐ working…` placeholder, edits it with live\n  tool-call status (`BrowserNavigate · https://wttr.in/...`,\n  `WebFetch · https://...`), and replaces it with the final reply when\n  the turn ends. Good for visibility into long-running tool chains.\n- `stream` — placeholder is progressively edited with the model's text\n  as it arrives, so the reply types itself out in front of the user.\n  Active tool calls surface as a faded tail line under the streaming\n  text.\n\nTelegram's Bot API rate-limits edits to ~1/sec/chat; `streamThrottleMs`\n(default 1000) coalesces rapid updates so we stay under the limit.\n\n**Text formatting** (`bots.telegram.parseMode`):\n\n- `html` *(default)* — the agent's markdown is converted to Telegram HTML\n  on the way out, so `**bold**`, `*italic*`, `` `code` ``, fenced code\n  blocks, `[links](https://…)`, headings, bullets and `> quotes` all\n  render as the user expects. Mid-stream tag balancing keeps live edits\n  valid; if Telegram still rejects the markup we silently fall back to\n  plain text rather than dropping the reply.\n- `plain` — send exactly what the agent emits. Use this if you want the\n  raw markdown markers visible (debugging, or if your persona instructs\n  the model to avoid markup entirely).\n\n**Per-chat isolation.** Each chat — a Telegram chatId, or the local REPL —\ngets its own task list, plan-mode flag, browser context, monitored\nprocesses, and SOUL.md persona. Two chats sharing a daemon never see each\nother's state.\n\n**Bot-side slash commands** (auto-completed in Telegram via\n`setMyCommands`):\n\n| Command  | What it does                                              |\n| -------- | --------------------------------------------------------- |\n| `/help`  | How to use the bot                                        |\n| `/status`| Provider, model, your tasks, plan mode, worktree          |\n| `/clear` | Forget conversation history                               |\n| `/reset` | Clear history + tasks + plan mode                         |\n| `/tasks` | List your tasks                                           |\n| `/plan`  | Toggle Plan Mode (read-only research mode)                |\n| `/soul`  | Show / `set` / `edit` / `clear` your personal persona     |\n\n## Voice messages\n\nSend the Telegram bot a voice note and it is transcribed before the agent\nsees it. The transcript is labelled, not passed off as typed text — the agent\nknows it was spoken, which is what makes \"I didn't quite catch that\" a\nsensible reply to a bad transcript. The recording is deleted as soon as it\nhas been read, whether transcription succeeded or not.\n\nTwo backends, because the two ways people actually run Whisper are a local\nbinary and an HTTP endpoint:\n\n```jsonc\n\"stt\": {\n  \"enabled\": true,\n  \"provider\": \"auto\",              // auto | command | openai-compatible | off\n  \"command\": \"\",                   // local CLI, see below\n  \"baseUrl\": \"\",                   // OpenAI-compatible /audio/transcriptions\n  \"model\": \"\",                     // sent to whichever backend runs\n  \"language\": \"\",                  // ISO code, or empty to auto-detect\n  \"timeoutSeconds\": 120,\n  \"maxFileMb\": 25\n}\n```\n\n`auto` prefers the command when one is set — a local binary costs nothing and\nsends nobody's voice anywhere. A pinned backend is never silently swapped for\nthe other one: being told `command` and quietly uploading the audio instead\nwould be a privacy decision made on your behalf.\n\n**Local command.** The template gets `{input}`, and optionally `{model}`,\n`{language}` and `{output_dir}`. Every value is quoted before substitution, so\na path with spaces stays one argument. Mention `{output_dir}` and the\ntranscript is read from the `.txt` left there; omit it and stdout is the\ntranscript.\n\n```bash\n# whisper-ctranslate2 (CUDA, writes a .txt)\n\"command\": \"whisper-ctranslate2 {input} --model {model} --language {language} --output_format txt --output_dir {output_dir}\"\n\n# whisper.cpp (prints to stdout)\n\"command\": \"whisper-cli -m ~/models/ggml-large-v3.bin -f {input} --no-timestamps\"\n```\n\n**HTTP.** Any endpoint that speaks OpenAI's audio API — Groq's free tier,\nOpenAI, a local `whisper-server`. The key, when the service needs one, is the\n`ASTERISK_STT_API_KEY` secret; a local server usually needs none.\n\n```jsonc\n\"stt\": { \"baseUrl\": \"https://api.groq.com/openai/v1\", \"model\": \"whisper-large-v3-turbo\" }\n```\n\nThe agent gets the same pipeline as a tool: `Transcribe` takes a path to any\naudio file and returns what was said, with optional per-call `language` and\n`model` overrides.\n\nLeave `language` empty unless auto-detection is getting it wrong — forcing a\nlanguage makes Whisper render other languages into that one.\n\n## MCP servers\n\nAsterisk speaks the [Model Context Protocol](https://modelcontextprotocol.io)\nas a **client**. Add servers via `/mcp` (or hand-edit\n`~/.asterisk/config.json` `mcpServers[]`):\n\n- **stdio**: `{ name, transport: \"stdio\", command, args, env }`\n- **http**: `{ name, transport: \"http\", url, headers }`\n\nTools exposed by connected MCP servers are namespaced as\n`<servername>__<toolname>` and added to the agent's tool registry on\nstartup. Failures during connect surface in `/mcp list`, never crash\nstartup.\n\n## Architecture\n\n```\nbin/asterisk         # Bash dispatcher: REPL | start | stop | status | logs | configure\nsrc/\n├── entrypoints/     # cli.tsx · daemon.ts · control.ts · configure.tsx\n├── repl/            # Ink REPL — App, CommandMenu, MarkdownText,\n│                    # WorkingIndicator, forms/, inline-image\n├── agent/loop.ts    # tool-use loop with retry, abort, terminal-reason,\n│                    # rules, hooks, sub-agents, per-tool timeout,\n│                    # tool concurrency, context compaction\n├── agent/           # compaction.ts · file-history.ts · output-store.ts\n│                    # persistence.ts — conversation save/restore\n├── providers/       # openai-compatible (default) · anthropic · model-detect\n├── tools/           # bash · read · write · edit · grep · glob ·\n│                    # browser/ (Playwright) · webfetch · websearch ·\n│                    # tasks · subagent · planmode · worktree · notify ·\n│                    # monitor · ask · schedule · tool-search ·\n│                    # approval · bash-gate · bash-permissions ·\n│                    # command-parse · bash-safety · concurrency ·\n│                    # code/ (RunCode: lexer · parser · interpreter · bridge)\n├── commands/        # slash command registry (visual flows)\n├── config/          # zod schema · loader · interactive wizard\n├── daemon/          # pidfile · logger · lifecycle · scheduler\n├── i18n/            # interface language (en · ru) — user-facing strings only\n├── bots/            # adapter contract · telegram\n├── mcp/             # client · manager (stdio + Streamable HTTP)\n├── hooks/runner.ts  # lifecycle hooks (before/after_tool, …)\n├── rules/loader.ts  # markdown rules → system prompt\n├── skills/          # bundled.ts (5) + loader (user/project SKILL.md)\n├── soul/loader.ts   # SOUL.md (user / per-chat / project) → system prompt\n├── agent/context.ts # per-session ALS — chatId scopes tasks/plan/soul/etc\n└── utils/           # retry · path\n```\n\nThe provider abstraction is provider-neutral: tools and the agent loop don't\nknow whether they're talking to a local server or Anthropic. The same loop drives\nthe REPL and each per-chat conversation in the daemon.\n\n## Configuration reference\n\nConfiguration lives in `~/.asterisk/asterisk.db` (SQLite, mode 0600). Edit it\nwith `asterisk web`, `asterisk configure`, or the REPL slash commands — not by\nhand.\n\nAn existing `config.json` from an older install is imported automatically on\nfirst run and renamed to `config.json.migrated`. The same shape is still what\nthe panel's **Download JSON** button produces and **Upload JSON** accepts:\n\n```jsonc\n{\n  \"provider\": \"openai-compatible\",            // or \"anthropic\"\n  \"providerFallback\": [],                     // e.g. [\"anthropic\"] — tried when the primary is unreachable\n  \"openaiCompatible\": {                       // llama.cpp / LM Studio / vLLM / Ollama's /v1 / …\n    \"baseUrl\": \"http://127.0.0.1:8080/v1\",\n    \"model\": \"\",                              // blank = ask the server (recommended)\n    \"contextWindow\": 0,                       // 0 = take the window the server reports\n    \"maxTokens\": 0,                           // 0 = let the server decide\n    \"modelTimeoutMs\": 300000,\n    \"modelIdleTimeoutMs\": 90000\n  },\n  \"anthropic\": { \"model\": \"claude-haiku-4-5\" },\n  \"bots\": {\n    \"telegram\": {\n      \"enabled\": false,\n      \"allowedUserIds\": [],\n      \"streamMode\": \"final\",                  // \"final\" | \"status\" | \"stream\"\n      \"streamThrottleMs\": 1000,               // min gap between editMessageText calls\n      \"parseMode\": \"html\"                     // \"html\" renders markdown · \"plain\" leaves it literal\n    }\n  },\n  \"daemon\": { \"logLevel\": \"info\", \"heartbeatSeconds\": 60 },\n  \"vision\": {                                 // images sent to the model\n    \"enabled\": true,                          // off for a text-only model\n    \"maxPerTurn\": 2,\n    \"maxBytes\": 4000000,\n    \"keepInHistory\": 2                        // older ones become a note\n  },\n  \"sandbox\": {                                // what a command may reach — see \"Sandbox\"\n    \"mode\": \"auto\",                           // \"auto\" | \"required\" | \"off\"\n    \"network\": true,                          // off blocks installs, git push, curl\n    \"writablePaths\": []                       // beyond the workspace and /tmp\n  },\n  \"permissions\": {                            // what Bash may run — see \"Permissions\"\n    \"mode\": \"ask\",                            // \"ask\" | \"allowlist\" | \"unrestricted\"\n    \"allow\": [],                              // extra rules, e.g. [\"npm test\", \"docker ps\"]\n    \"deny\": [],                               // refused outright, beats every allow\n    \"headless\": \"deny\",                       // when nobody can answer at all\n    \"chatApprovals\": true,                    // ask in the chat, with buttons\n    \"timeoutSeconds\": 90                      // how long that prompt waits\n  },\n  \"web\": {\n    \"host\": \"127.0.0.1\",\n    \"port\": 4321,\n    \"authRequired\": true,\n    \"openBrowser\": true\n  },\n  \"stt\": {                                    // voice messages — see \"Voice messages\"\n    \"enabled\": true,\n    \"provider\": \"auto\",                       // auto | command | openai-compatible | off\n    \"command\": \"\",                            // local whisper CLI template\n    \"baseUrl\": \"\",                            // or an OpenAI-compatible endpoint\n    \"model\": \"\",\n    \"language\": \"\",                           // empty = auto-detect\n    \"timeoutSeconds\": 120,\n    \"maxFileMb\": 25\n  },\n  \"mcpServers\": [],\n  \"hooks\": []\n}\n```\n\n### Secrets\n\nSecrets are stored in the database and set through `asterisk web` or\n`asterisk configure`. They are resolved highest-priority-first:\n\n1. the process environment,\n2. the database,\n3. a legacy `~/.asterisk/secrets.env`, read as a fallback and imported once.\n\n```bash\nANTHROPIC_API_KEY=\"...\"\nASTERISK_TELEGRAM_BOT_TOKEN=\"...\"\nASTERISK_NOTIFY_URL=\"...\"     # optional — used by PushNotification tool\n```\n\nExporting one of these in your shell overrides whatever is stored, which is\nuseful for one-off runs and CI. Note this is the reverse of the pre-database\nbehaviour, where `secrets.env` won.\n\nOverride the config root with `ASTERISK_HOME=/path/to/dir`.\n\n## Permissions\n\n**This is a consent boundary, not a sandbox.** An approved command runs as\na normal child process with the full privileges of the user who started\nAsterisk. What the boundary buys is that nothing with unreviewed effects\nruns without someone saying yes — it is not containment, and a command you\napprove can do anything you could do.\n\nRead-only commands run immediately. Anything else prompts:\n\n```\n🔒  Approve this command?\n\n    npm test -- --coverage\n\n    Needs approval because \"npm test -- --coverage\" is not on the allowlist.\n\n  › Allow once      Run it this time only.\n    Allow always    Remember npm test and stop asking.\n    Deny            Refuse, and tell the agent not to retry.\n```\n\nCommands are split into the segments the shell would actually run before any\nrule is consulted, so `git status && rm -rf ~` needs approval even though\n`git status` alone does not. Anything the parser cannot statically resolve —\ncommand substitution, backticks, variable expansion, here-docs, subshells,\nredirection to a real path — is never auto-approved, whatever the rules say.\nRules are matched positionally and are path-sensitive: a rule for `git` does\nnot hand approval to `./git`.\n\n| `permissions.mode` | Behaviour                                              |\n| ------------------ | ------------------------------------------------------ |\n| `ask` (default)    | Allowlisted commands run; everything else prompts       |\n| `allowlist`        | Anything not allowlisted is refused, never prompted     |\n| `unrestricted`     | No boundary — the pre-0.4 behaviour, opt in explicitly  |\n\n**Prompts in a chat.** A bot turn is not unattended — there is a person at the\nother end of it. When a command needs a decision, the bot asks in the same chat\nwith three buttons: allow once, allow from now on (which remembers the rule),\nor deny. Only a user on `bots.telegram.allowedUserIds` may press them, so a\ngroup chat is not a way in, and an unanswered question is refused when\n`permissions.timeoutSeconds` runs out. Set `permissions.chatApprovals` to false\nto turn this off and treat every bot turn as unattended.\n\n**Genuinely unattended runs.** Scheduled jobs, and transports that cannot show a\nprompt, have nobody to ask — `permissions.headless` decides for them. It\ndefaults to `deny`: a command that would have prompted is refused, with a\nmessage telling the user which rule to add. Set it to `allow` only if you accept\nthat unattended sessions then have no boundary at all.\n\nManage it with `/permissions` in the REPL, or the **Permissions** section of\n`asterisk web`:\n\n```bash\n/permissions                    # effective policy, config rules, remembered grants\n/permissions builtin            # the built-in read-only set\n/permissions allow \"npm test\"   # add a rule\n/permissions deny  \"git push\"   # refuse outright, ahead of every allow\n/permissions revoke             # pick a remembered grant to forget\n```\n\nThe 14-regex denylist in `bash-safety.ts` still runs first, but it is defence\nin depth rather than the boundary: `rm -r -f /`, `$(echo rm) -rf /` and\n`sh -c '…'` all walk straight through it. The permission gate is what stops\nthem.\n\n## Sandbox\n\nPermissions decide *whether* a command runs. The sandbox decides what it can\nreach once it does — and the two are independent, so an allowlisted read-only\ncommand is confined too.\n\n`Bash` runs under **bubblewrap** on Linux and **`sandbox-exec`** (seatbelt) on\nmacOS. The whole filesystem is bound read-only; the workspace and `/tmp` are\nwritable; `/dev` and `/proc` are fresh, so a command cannot see every other\nprocess on the machine. Notably `~/.asterisk` is *not* writable: a command\ncannot rewrite the secret store or the permission grants that let it run.\n\n**A backend is not trusted until it proves itself.** On first use Asterisk\nruns a probe that tries to write somewhere it must not be able to, and refuses\nto use the backend unless that write fails. A sandbox that silently does not\nsandbox is worse than none — it moves you from cautious to confident without\nmoving the security — so \"installed\" is never taken as \"working\".\n\n| `sandbox.mode` | Behaviour                                                    |\n| -------------- | ------------------------------------------------------------ |\n| `auto` (default) | Confine when a probed backend exists, run unconfined otherwise |\n| `required`     | Refuse to run commands at all when no backend is available     |\n| `off`          | Never confine                                                  |\n\n`sandbox.network` (default on) controls network access — turning it off blocks\npackage installs and `git push` along with everything else.\n`sandbox.writablePaths` adds paths beyond the workspace, and governs the\nin-process `Write` and `Edit` as well as the shell — one setting, one boundary.\nThe two differ in exactly one place: `/tmp` is writable by the shell and not by\nthe file tools, because reaching for it through `Bash` costs an approval prompt\nand reaching for it through `Write` costs nothing.\n\n`/doctor` reports which backend is active and why. If it says `none`, you are\nnot sandboxed:\n\n```\nSecurity\n  ✓ Bash perms mode ask · unattended runs deny\n  ✓ Sandbox    bubblewrap — bwrap passed a containment probe\n```\n\nOn Linux, `apt install bubblewrap` (or your distro's equivalent) is all it\ntakes. Its seatbelt counterpart ships with macOS.\n\n## Reliability\n\n**Provider fallback.** `providerFallback` lists backends to try, in order, when\nthe primary one cannot answer — a laptop whose model server is not running falls\nthrough to a configured Anthropic key instead of failing every turn. Only\navailability failures step down the chain (network, 5xx, overloaded, rate\nlimit, auth); a rejected request is *not* replayed elsewhere, because it would\nfail there too and the switch would hide the real error. A reply that has\nalready begun streaming is never restarted on another backend. The chain\nreports the smallest context window of its links, since the history is built\nonce and may be answered by any of them.\n\nThe agent loop wraps every model call in retry logic with exponential\nbackoff + jitter, honours the `Retry-After` header, classifies HTTP errors\ninto kinds (`rate-limit`, `overloaded`, `server`, `network`, `auth`,\n`bad-request`, `context-overflow`, `aborted`), and threads `AbortSignal`\nend-to-end so Ctrl+C cleanly cancels the in-flight provider call, the\nsleep, and any running tool.\n\nEvery tool call is timeboxed (default 120s) by an inner `AbortController`\nthat ANDs the parent signal with the timeout — runaway shell commands\ncan't lock the loop. Press **ESC** in the REPL to abort the current turn\nand clear the message queue.\n\n**Agent loop hardening:**\n\n- **Tool concurrency** — concurrency-safe tools (Read, Grep, Glob,\n  WebFetch, WebSearch, …) run in parallel via `Promise.all` when the\n  model emits multiple in one turn.\n- **Context compaction** — the budget is 60% of the context window the\n  active provider reports. Over it, old tool results and long text blocks\n  are truncated while keeping the 6 most recent messages intact. If that is\n  not enough, the oldest messages are dropped — and **replaced by a summary\n  the model writes of what they contained**, so a long session keeps the\n  decisions that shaped it instead of just a count of what was lost. A\n  summariser that fails costs context and nothing else; the turn continues\n  with the plain notice. Token counting is a character-class estimate, not\n  `chars / 4`: CJK counts near one token per character and punctuation-dense\n  code above the prose rate, because under-counting those is what silently\n  overflows a window.\n- **Large result persistence** — tool outputs > 8 KB are saved to\n  `~/.asterisk/outputs/` with a 500-char preview kept in context.\n- **Prompt caching** — Anthropic provider sends the system prompt with\n  `cache_control: { type: 'ephemeral' }` for cross-turn caching.\n- **Bash permissions** — read-only commands run; anything else needs the\n  user's approval. See [Permissions](#permissions), including what it\n  deliberately does not promise.\n- **File history** — Write/Edit tools snapshot files before overwriting;\n  stored in `~/.asterisk/file-history/`.\n- **Conversation persistence** — daemon saves per-chat history to\n  `~/.asterisk/conversations/` as JSON, restores on reconnect, 7-day\n  expiry.\n\n## Roadmap\n\nSee [ROADMAP.md](./ROADMAP.md) for the prioritised list of upcoming work\n— skill marketplace, image content blocks, multi-agent\ncoordinator, and others.\n\n## Limitations\n\n- Tasks, plan-mode, and worktree state live in memory; daemon restart\n  wipes them. Conversation history now persists across daemon restarts\n  (7-day expiry), but task lists do not.\n- **The sandbox covers `Bash` only.** `Read`, `Write` and `Edit` run\n  in-process, so no child-process sandbox can reach them. They share the same\n  writable set — one `sandbox.writablePaths` setting governs both — but a path\n  check is a check, while bubblewrap is a kernel boundary. Only the file tools\n  can be turned off with `ASTERISK_NO_WORKSPACE_GUARD=1`; `Bash` stays confined\n  and still needs approval.\n- **Reads are not confined.** The sandbox restricts what a command can\n  *change*, not what it can see. A command you approve can read any file your\n  user can.\n\n## Provenance\n\nAsterisk is an independent, clean-room implementation, written from published\nAPI documentation and public npm packages. [PROVENANCE.md](./PROVENANCE.md)\nsets out what that means concretely, which sources were used, what was\nconsulted as an architectural reference and what was not, and how to report\nanything you believe was copied when it should not have been.\n\n## License\n\n[Apache 2.0](./LICENSE).\n","readmeFilename":"README.md"}