npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@brain-ai/teams-bot

v0.0.40

Published

Unified CLI for brain: setup, start, stop, logs, status

Readme

Brain

A unified CLI that turns Microsoft Teams into a personal AI assistant interface. Pick the agent backend per conversation: the Claude Agent SDK (default — in-process, full MCP / hooks / sub-agents) or the GitHub Copilot CLI (per-turn subprocess). One command connects to the shared gateway, another provisions your own — then you chat in Teams with full tool use, persistent sessions, and scheduled tasks.

How It Works

Brain follows a 3-tier WebSocket relay architecture: a shared Cloud Gateway on Azure routes Teams messages over persistent WebSocket connections to each user's local machine, where Claude processes them.

flowchart TB
    subgraph teams ["Microsoft Teams"]
        user(["User @mentions bot"])
    end

    subgraph bfs ["Azure Bot Service"]
        bf["Bot Framework Service\n(message routing)"]
    end

    subgraph azure ["Azure App Service (shared)"]
        gw["Cloud Gateway (bot-app)\nTeams SDK · WSS relay\nOID routing · device binding\nofflinebuffer · picker"]
    end

    subgraph local ["User's Machine(s)"]
        cli["brainbotbeta CLI\nMSAL auth · daemon\ntoken refresh · LLM proxy"]
        server["local-server\nClaude SDK / Copilot CLI · device-id\nSQLite · scheduler · MCP tools"]
    end

    user -- "message" --> bf
    bf -- "reply" --> user
    bf -- "POST /api/messages" --> gw
    gw -- "reply\n(Managed Identity, no secrets)" --> bf

    server -- "WSS /connect\n(Entra ID JWT + x-brain-device-id)" --> gw
    gw -- "MessageFrame" --> server
    server -- "ResponseFrame\nTypingFrame · PushFrame" --> gw

    cli -. "spawns + manages" .-> server
    cli -. "refreshes tokens\n(relay JWT · Graph token)" .-> server

Key insight: the local-server connects outbound to the Gateway — no port forwarding, no tunnels, works behind any NAT/firewall. When the local-server is offline, the Gateway buffers messages (up to 100/user, 1h TTL) and replays them on reconnect.

Multi-device routing: each local-server install carries a stable deviceId (UUIDv4 at ~/.brainbotbeta/device-id). The Gateway can route the same OID to different devices for different Teams conversations — bound at first message (silent auto-bind if 1 device online, picker card if 2+). See Multi-Device Routing.

Prerequisites

  • Node.js >= 22.13.0 (for built-in node:sqlite)
  • Azure CLI (az) — logged in with az login
  • Claude Code CLI (claude) — installed via winget install Anthropic.ClaudeCode or npm install -g @anthropic-ai/claude-code (required for the default claude backend)
  • GitHub CLI (gh) — logged in with gh auth login (needed to download from private repo)
  • GitHub Copilot CLI (copilot) — optional, only required to use the copilot agent backend (winget install GitHub.CopilotCLI or npm install -g @github/copilot)
  • A Microsoft 365 account with Teams
  • A GitHub account (for Copilot LLM proxy authentication and the optional Copilot agent backend)

All prerequisites are auto-installed by the install script if missing (via winget).

Quick Start (End Users)

Install directly from npm:

npm install -g @brain-ai/teams-bot

Then start the daemon:

brainbotbeta start --gateway wss://brain-bot-beta.azurewebsites.net/connect

Alternatively, use the install script which also handles prerequisites and proxy setup. Requires gh CLI installed and authenticated:

Remove-Item "$env:TEMP\*.ps1" -ErrorAction SilentlyContinue; gh release download --repo liuwentong_microsoft/teamsclaw --pattern "*.ps1" --dir $env:TEMP; powershell -ExecutionPolicy Bypass -File "$env:TEMP\install.ps1"

The install script will:

  1. Check and auto-install missing prerequisites (Node.js, gh, Claude Code, Azure CLI)
  2. Download the latest release from GitHub
  3. Install brainbotbeta globally via npm
  4. Test Claude API access — start LLM proxy if needed
  5. Start the daemon and connect to the gateway

For upgrades, run the same command again — it auto-detects the old version, stops the daemon, and installs the new one.

Quick Start (Developers)

# Install dependencies and build all four packages (shared + CLI + bot-app + local-server)
npm install
npm run build

# Link the CLI globally so `brainbotbeta` is available in your PATH
npm link

# Connect to an existing shared gateway (first run opens browser for MSAL auth):
brainbotbeta start --gateway wss://brain-bot-beta.azurewebsites.net/connect

# Or self-deploy your own gateway:
brainbotbeta deploy

# Then message the bot in Teams!

Starting the daemon later — if auth is cached, brainbotbeta start silently refreshes the token, reconnects, and starts the daemon. To also have it come back after a reboot, run brainbotbeta start --auto-start once: that opts in to OS login auto-start and sets up + bakes in the LLM proxy so the bot can still reply after boot.

CLI Commands

| Command | Description | |---------|-------------| | brainbotbeta start --gateway <url> | First run: discover MSAL config, browser auth, start daemon | | brainbotbeta start | Subsequent runs: silent MSAL refresh, start daemon | | brainbotbeta start --auto-start | Opt in to OS login auto-start; also sets up + starts the LLM proxy and bakes --proxy into the boot launcher so the bot still replies after reboot | | brainbotbeta start --no-auto-start | Deprecated no-op (auto-start is opt-in); still accepted so existing boot launchers keep parsing | | brainbotbeta start --name <name> | Persist a display name for this device (1–64 chars). Shown in the device picker when you run multiple local-servers under one account. | | brainbotbeta stop | Stop the daemon and clean up state | | brainbotbeta status | Show gateway URL, PID, uptime, auth status, proxy status | | brainbotbeta logs | Tail real-time daemon log file | | brainbotbeta proxy start | GitHub device-flow auth, spawn LLM proxy, configure Claude | | brainbotbeta proxy stop | Stop proxy, restore Claude settings | | brainbotbeta proxy status | Show proxy PID, port, health | | brainbotbeta proxy logs | View proxy logs (-f for follow, --err for stderr) | | brainbotbeta service install | Auto-start brainbotbeta start on login (systemd/launchd/Windows Startup) and start it now | | brainbotbeta service install --proxy | Same, and also auto-start the LLM proxy on login | | brainbotbeta service uninstall | Remove auto-start (leaves a running daemon untouched) | | brainbotbeta service status | Show whether auto-start is installed | | brainbotbeta deploy | Self-deploy: provision Azure resources + register Teams app + auto-start | | brainbotbeta upgrade | Upgrade to the latest version from npm (restarts daemon if running) | | brainbotbeta deploy --code-only | Redeploy code only (skip provisioning) |

Auto-start on loginbrainbotbeta start registers an OS launcher by default so the daemon reconnects to the gateway automatically after a reboot. It uses a systemd user unit on Linux, a launchd LaunchAgent on macOS, and a hidden VBScript launcher in the Startup folder on Windows. Use brainbotbeta service install to manage it explicitly (for example --proxy), or remove it with brainbotbeta service uninstall.

Project Structure

brain/
├── shared/                    # Shared types & utilities (dual CJS/ESM, zero runtime deps)
│   └── src/
│       ├── relay-types.ts     # Wire protocol frame types (canonical source)
│       ├── config-types.ts    # ClaudeConfig, SkillsConfig, DeepPartial<T>
│       ├── config-defaults.ts # DEFAULT_CLAUDE_CONFIG, DEFAULT_SKILLS_CONFIG
│       ├── deep-merge.ts      # deepMerge() — recursive config merge
│       └── expand-tilde.ts    # expandTilde() — ~ → homedir in object trees
├── cli/                       # CLI tool (runs locally, never deployed)
│   ├── commands/              # start, stop, status, logs, proxy, deploy, upgrade
│   ├── runtime/               # Daemon entry point + child processes
│   │   ├── daemon-entry.ts    # Orchestrator: tokens → server → health monitor
│   │   ├── token-refresh.ts   # Periodic relay JWT (50min) + Graph token refresh
│   │   ├── server-env.ts      # Builds env vars and spawns local-server
│   │   └── llm-proxy/         # LLM proxy (Copilot → Anthropic translation)
│   │       ├── server.ts      # HTTP server (localhost:18976)
│   │       ├── converters.ts  # Anthropic ↔ OpenAI format conversion
│   │       ├── stream-converter.ts  # SSE event translation
│   │       └── token-manager.ts     # GitHub → Copilot token exchange
│   ├── lib/                   # Shared utilities (MSAL, config, paths)
│   │   ├── msal.ts            # MSAL PKCE auth (interactive + silent)
│   │   ├── load-config.ts     # BrainConfig read/write
│   │   ├── proxy-lifecycle.ts # LLM proxy start/stop (detached spawn + PID file)
│   │   └── process-utils.ts   # isProcessAlive() for daemon health checks
│   └── index.ts               # CLI entry point (commander)
├── bot-app/                   # Cloud Gateway (deployed to Azure App Service)
│   ├── deploy/                # Self-deploy scripts (Bicep, App Registration, Teams App)
│   └── src/
│       ├── app/app.ts         # Teams message handler, image extraction, relay delivery
│       ├── relay/             # WebSocket relay infrastructure
│       │   ├── connect-endpoint.ts   # /connect WSS upgrade + /connect/config
│       │   ├── connection-handler.ts # Per-connection lifecycle, heartbeat, frame dispatch
│       │   ├── jwt-validator.ts      # Entra ID JWT validation (jose)
│       │   ├── client-registry.ts    # OID → connections map (max 5/user)
│       │   ├── reply-context-store.ts# channel_id → ReplyContext (1h TTL, OID-guarded)
│       │   ├── offline-buffer.ts     # Message buffer for offline users (100/user, 1h TTL)
│       │   ├── outbound-router.ts    # Response/typing/push → Teams delivery
│       │   ├── message-delivery.ts   # Inbound message → best connection or buffer
│       │   ├── teams-conversation-store.ts  # In-memory conversation refs for push
│       │   ├── client-connection.ts  # WebSocket wrapper (serialized sends)
│       │   └── wire-frames.ts        # Server-side frame encode/decode
│       ├── config.ts          # Environment config
│       └── index.ts           # HTTP server entry point
├── local-server/              # Local AI server (runs on your machine)
│   └── src/
│       ├── relay/             # WebSocket client to Gateway
│       │   ├── cloud-channel.ts      # WSS client, reconnection, send queue
│       │   ├── html-to-markdown.ts   # Teams HTML → Markdown conversion
│       │   └── wire-frames.ts        # Client-side frame encode/decode
│       ├── agent/             # Agent backends (Claude SDK + Copilot CLI dispatcher)
│       │   ├── index.ts       # processMessage dispatcher + Claude SDK query path (resolveBackend, session resume, streaming)
│       │   ├── copilot-runner.ts  # GitHub Copilot CLI driver (spawns `copilot --output-format json`, parses JSONL)
│       │   ├── context.ts     # Builds conversation context from DB history
│       │   ├── hooks.ts       # PreCompact hook: archive transcripts before compaction (Claude backend only)
│       │   └── system-prompt.ts  # NECESSARY + OVERRIDABLE layers, composeSystemPrompt()
│       ├── db/                # SQLite persistence (node:sqlite)
│       │   ├── schema.ts      # Tables: messages, sessions, tasks, dirs, images, workspaces
│       │   ├── messages.ts    # Message + image CRUD
│       │   ├── sessions.ts    # Session persistence
│       │   ├── tasks.ts       # Scheduled task CRUD
│       │   ├── state.ts       # KV store for router state
│       │   └── directories.ts # Per-session directory CRUD
│       ├── mcp/               # Custom MCP tools for Claude
│       │   ├── push-message.ts     # Proactive messaging to Teams
│       │   ├── schedule-task.ts    # Schedule recurring/one-shot tasks
│       │   ├── list-tasks.ts       # List scheduled tasks
│       │   ├── manage-tasks.ts     # Pause, resume, cancel, update tasks
│       │   └── manage-dirs.ts      # Add, remove, list per-session directories
│       ├── scheduler/         # Task execution engine
│       │   ├── index.ts       # Polling loop (60s, setTimeout chain)
│       │   ├── runner.ts      # Execute task → push result to Teams
│       │   └── next-run.ts    # Compute next run time (cron/interval/once)
│       ├── index.ts           # Message routing, # commands, concurrency pipeline
│       └── graph.ts           # Graph API: channels, messages, threads
├── scripts/
│   ├── postinstall.js         # Writes package paths to config, installs sub-project deps
│   ├── pack.js                # Build distributable .tgz (excludes bot-app)
│   ├── pack-deploy.js         # Build deploy-kit .zip for self-host
│   └── install.ps1            # Full Windows installer (auto-installs prerequisites)
├── docs/                      # Debug & reference documentation
├── tests/                     # Graph API integration tests
├── package.json               # Root package: CLI dependencies + build scripts
└── tsconfig.json              # Root TypeScript config (CommonJS for CLI)

Architecture

3-Tier WebSocket Relay

The system has three independent tiers connected by a WebSocket relay protocol:

Teams User ──@bot──> Bot Framework Service ──POST──> Cloud Gateway (Azure App Service)
                            ▲                           │ Route by OID
                            │ reply (Managed Identity)  │
                            └───────────────────────────┤
                                                   WSS <┤ /connect (Entra ID JWT)
                                                        └──> WSS
                                        User's local-server       (runs locally)
  1. Cloud Gateway (bot-app/) — Receives Teams messages, routes by user OID over WebSocket to the correct local-server. Deploys once, shared by all users.
  2. Local AI Server (local-server/) — Runs on the user's machine. Processes messages via the configured agent backend (Claude Agent SDK in-process or copilot CLI spawned per turn), sends replies back through the relay.
  3. CLI (cli/) — Manages the local lifecycle: brainbotbeta start authenticates via MSAL, starts the daemon (token refresh + local-server auto-respawn), optionally runs the LLM proxy.

Four Independent Packages

| Package | Runs On | Module System | Target | Purpose | |---------|---------|---------------|--------|---------| | shared | Build-time | Dual CJS/ESM | ESNext | Wire protocol types, config types, pure utilities | | cli | Local machine | CommonJS | ES2022 | CLI commands, daemon, MSAL auth, LLM proxy | | bot-app | Azure App Service | ESM (NodeNext) | ESNext | Teams message routing, WSS relay, offline buffer, push delivery | | local-server | Local machine | ESM (NodeNext) | ESNext | Claude Agent SDK / Copilot CLI dispatcher, SQLite, MCP tools, task scheduler |

Isolation rules: bot-app and local-server never import each other — they share types through shared/ via file: dependency. bot-app must NOT depend on @anthropic-ai/claude-agent-sdk. local-server must NOT depend on @microsoft/teams.* or @azure/identity.

WebSocket Protocol

JSON text frames, snake_case fields, discriminated by type. Protocol version: 1.

Server → Client (Gateway → local-server):

| Frame | Purpose | |-------|---------| | connected | Handshake confirmation (protocol_version, buffered_count) | | buffered_start / buffered_end | Marks offline buffer replay boundaries | | message | User message from Teams (channel_id, sender_id, content, attachments, session_key, ...) | | ping | Heartbeat (every 45s; client must reply pong within 15s) | | error | Error notification |

Client → Server (local-server → Gateway):

| Frame | Purpose | |-------|---------| | pong | Heartbeat reply | | response | Final reply to a message (consumes ReplyContext) | | typing | Typing indicator (keeps ReplyContext alive) | | push | Proactive message (no ReplyContext needed, routed by session_key or most recent conversation) | | notification | Intermediate notification | | chunk | Streaming token chunk (reserved) |

Connection lifecycle:

  1. Client opens WSS /connect with Authorization: Bearer {jwt}
  2. Gateway validates JWT, extracts OID, sends connected
  3. If offline buffer has messages: buffered_start → N × messagebuffered_end
  4. Steady state: message/response/typing/push exchange
  5. Heartbeat: ping every 45s, pong within 15s or disconnect

Message Processing Pipeline

Teams @mention → Bot Framework → Gateway (bot-app)
    → OID extraction, image download, MessageFrame creation
    → Route by DeviceBindingStore[sessionKey]:
        ├─ bound + device online    → deliver to bound device
        ├─ bound + device offline   → buffer for that deviceId
        ├─ unbound + 1 device       → auto-bind + deliver
        ├─ unbound + 2+ devices     → post Adaptive Card picker (PendingMessageStore holds msg)
        └─ unbound + 0 devices      → buffer untargeted (drain to first reconnect)
        ↓
local-server receives MessageFrame
    → Strip @mentions, HTML → Markdown
    → Persist images to disk, store message in SQLite
    → Process in background:
        ├─ withConcurrencyLimit (max 5 parallel)
        ├─ withLock (per-conversation mutex)
        ├─ withRetry (3 attempts, exponential backoff)
        └─ processMessage → resolveBackend(conversationKey)
            ├─ claude  → @anthropic-ai/claude-agent-sdk query() (in-process, MCP/hooks/sub-agents)
            └─ copilot → spawn `copilot --output-format json ...` (per-turn subprocess, JSONL stdout)
            → Typing indicators every 3s while the backend works
            → Result: sendResponse → Gateway → Teams reply

Daemon Lifecycle

When you run brainbotbeta start, the daemon:

  1. Acquires relay JWT via MSAL (interactive on first run, silent thereafter)
  2. Acquires Graph API token via Azure CLI
  3. Writes tokens to ~/.brainbotbeta/auth/ (mode 0o600)
  4. Spawns the local-server process with GATEWAY_URL + token file paths
  5. Starts periodic token refresh (relay JWT + Graph token every 50 min)
  6. Monitors local-server health (auto-respawn with exponential backoff, max 5 retries)
  7. Writes state to ~/.brainbotbeta/config/state.json

On brainbotbeta stop: CLI sends SIGTERM to the daemon PID → daemon stops refresh loops, kills local-server, removes state file.

LLM Proxy (Copilot → Anthropic)

An optional local HTTP proxy that lets the Claude Agent SDK use GitHub Copilot as its LLM backend:

Claude Agent SDK → POST /v1/messages (Anthropic format)
    → LLM Proxy (localhost:18976)
        → Convert Anthropic → OpenAI Chat Completions format
        → POST api.githubcopilot.com/chat/completions (with Copilot JWT)
        → Convert OpenAI → Anthropic format
    ← Return to Claude Agent SDK

Managed independently via brainbotbeta proxy start/stop. Uses GitHub OAuth device flow for auth. Runs as a detached process tracked by PID file.

Security

Authentication Chain

Every hop is independently authenticated. No link relies on "being internal" for security.

| Hop | Mechanism | |-----|-----------| | Teams → Bot Framework | Bot Framework JWT (validated by Teams SDK); bot authenticates via User-Assigned Managed Identity (no secrets) | | Bot Framework → Gateway | Channel validation: rejects non-msteams channels to prevent DirectLine OID spoofing | | Gateway → local-server | Entra ID JWT validated at WebSocket handshake (JWKS, issuer, audience checks via jose) | | local-server → Gateway | Same JWT (presented at connection time); OID extracted and used for all routing | | Response routing | ReplyContext OID validation: Gateway only routes responses to the OID that sent the original message | | Push routing | TeamsConversationStore OID validation: proactive messages only reach conversations owned by the sender's OID |

Key Security Properties

  • Zero secrets: MSAL PKCE flow with Entra ID JWTs — no client secrets stored anywhere
  • Cross-user isolation: OID-based routing with validation at every hop
  • Ephemeral tokens: Relay JWT refreshed every 50 min, Graph token refreshed every 50 min
  • File permissions: All token files written with 0o600
  • Dev bypass: Triple-guarded (NODE_ENV=development + RELAY_BYPASS_TOKEN non-empty + exact match)

Azure App Service Hardening

  • IP restrictions: Only AzureBotService + AzureCloud Service Tags allowed; default deny
  • SCM lockdown: scmIpSecurityRestrictionsDefaultAction: Deny — no public Kudu access
  • HTTPS only: Enforced at App Service level
  • Managed Identity: User-Assigned — no client secrets to rotate

Multi-Device Routing

You can run local-server on more than one machine under the same Microsoft 365 account (e.g. work laptop + Cloud PC). Each Teams conversation binds to one of those devices; subsequent messages in the same conversation always go to the bound device.

How a Teams conversation gets bound

When the first message of a new conversation arrives:

| Devices online | Behavior | |---|---| | 0 | Buffered (💤 queued). When the first device reconnects, the message drains to it and the conversation binds to that device's deviceId | | 1 | Silent auto-bind to the sole device. No picker. Indistinguishable from single-device use. | | 2+ | Gateway posts an Adaptive Card listing every online device by name. Original message held in PendingMessageStore. User clicks → binding recorded → held message delivered. |

Subsequent messages in the same conversation always go to the bound device. If the bound device is offline, the message is buffered for that specific deviceId and drained when it reconnects.

Device identity

  • deviceId: opaque UUIDv4 persisted at ~/.brainbotbeta/device-id (mode 0o600) on first start. Stable across restarts; survives brainbotbeta upgrade. Regenerated if the file is missing (e.g. fresh install or wiped ~/.brainbotbeta/).

  • deviceName: human-readable display string sent in the picker. Resolution order:

    1. --name <name> passed to brainbotbeta start (persisted to config)
    2. BRAIN_DEVICE_NAME env var
    3. os.hostname()

    Display only — never used for routing.

Switching devices

The only way to (re)bind a conversation to a different device is to start a new Teams conversation with #new — switching mid-conversation would orphan the Claude session, SQLite history, and any open workspace state on the original device.

Legacy clients (older local-server versions)

A local-server build that predates the multi-device feature does not send x-brain-device-id / x-brain-device-name headers on the WSS upgrade. Such connections are accepted (Gateway generates an ephemeral per-connection UUID and marks the connection isLegacy = true), but they are never included in the picker and never bound. Messages are delivered to legacy connections only when no non-legacy device is online — MRU by last pong. Upgrade local-server to participate in routing.

Local Server Features

Agent Backends

The local server drives one of two agent backends per conversation. Both return the same AgentResult, so the rest of the pipeline (typing indicators, sendResponse, push notifications, session persistence in SQLite) is unchanged.

  • claude (default)@anthropic-ai/claude-agent-sdk in-process. Full MCP servers, PreCompact hooks, Agent Teams sub-agent orchestration, additional directories, session resume.
  • copilot — Spawns copilot --output-format json --allow-all -s ... per turn (@github/copilot npm package, separate binary). Prompt via stdin (avoids Windows arg-length limits); JSONL stdout parsed for assistant.message_delta (buffered into the final reply) and result (session id persisted for --resume next turn). Trades the MCP / hooks / sub-agents extension surface for Copilot's fully managed agent loop and Copilot-side model routing.

Resolution order (per call): per-conversation router_state['backend:<key>'] → global router_state['global:defaultBackend']config.claude.backend → fallback 'claude'. Switch with #backend (per-conversation) or #default-backend (global). Switching backends clears the persisted session id because Claude and Copilot session IDs are not interchangeable.

Binary resolution (Copilot only): COPILOT_CLI_PATH env var wins; otherwise PATH-based lookup, cached for process lifetime. 15-minute per-turn timeout.

Claude Agent SDK Capabilities (claude backend)

When backend = 'claude', the agent has access to:

  • Full tool access: File I/O (Read, Write, Edit), Bash, Glob, Grep, WebSearch, WebFetch, NotebookEdit, and more
  • Agent Teams: Sub-agent orchestration for parallel work (Task, TaskOutput, SendMessage, etc.)
  • Session persistence: Sessions are stored in SQLite and resumed across server restarts
  • Conversation context: History built from stored messages in the database
  • Transcript archiving: PreCompact hooks archive transcripts to markdown before context compaction
  • Per-workspace isolation: Each conversation gets its own working directory

Copilot CLI Capabilities (copilot backend)

When backend = 'copilot', the agent has access to Copilot's built-in tool set (file edits, shell, web fetch, the bundled github-mcp-server, etc.) and Copilot-side model routing. The custom MCP servers, PreCompact hooks, and Agent Teams sub-agents from the Claude backend are not available in this mode. Image attachments are forwarded via --attachment <path>; session resume uses --resume <id> and the result event's session id, persisted in the same sessions table.

Custom MCP Tools

| Tool | Description | |------|-------------| | push_message | Send proactive messages to any Teams conversation | | schedule_task | Create scheduled tasks (cron/interval/once, local timezone) | | list_tasks | View all scheduled tasks for the current conversation | | pause_task / resume_task / cancel_task / update_task | Full task lifecycle management | | add_dir / remove_dir / list_dirs | Manage per-session directories for the Claude agent |

Task Scheduler

The scheduler runs a polling loop (60-second interval, setTimeout chain) that:

  • Finds due tasks from SQLite
  • Executes them through the same Claude Agent SDK pipeline (with concurrency limits)
  • Pushes results to Teams via the push mechanism
  • Logs execution details to task_run_logs
  • Supports cron expressions, fixed intervals (minutes), and one-shot execution
  • Tasks can run in conversation context (shared session) or isolated (fresh session each time)

SQLite Database

All persistent data lives in ~/.brainbotbeta/data/brain.db:

| Table | Purpose | |-------|---------| | messages | Conversation history (keyed by activity ID) | | message_images | Image attachments linked to messages | | sessions | Agent session IDs for resume (one row per conversation; stores either a Claude SDK session ID or a Copilot CLI session ID depending on the active backend at write time) | | scheduled_tasks | Task definitions with schedule, status, and next run time | | task_run_logs | Execution history for each task run | | router_state | KV store for serviceUrl persistence, agent timestamps, default workspace | | session_directories | Per-session additional directories | | workspace_overrides | Per-conversation cwd override |

Special Commands

| Command | Description | |---------|-------------| | #new [topic] | Create a dedicated Teams channel for conversation isolation | | #add-dir <path> | Register an additional directory for Claude (takes effect next message) | | #remove-dir <path> | Unregister a directory | | #list-dirs | Show all active directories for this session | | #workspace <path> | Set per-conversation working directory | | #default-workspace <path> | Set global default working directory | | #list-sessions [<page>\|next\|prev] | List Claude sessions in this workspace (40/page). If AgentLink is running locally, each row also gets an [open] link into AgentLink's web UI for that exact session. | | #session <n> | Resume the Nth session from the most recent #list-sessions page | | #system-prompt [prompt] | Override the per-conversation system prompt (no arg clears). Overrides only the style/guidelines layer; identity and tool knowledge are preserved. | | #default-system-prompt <prompt> | Replace the global default overridable layer | | #reset-system-prompt | Restore the built-in default overridable layer | | #backend [claude\|copilot\|reset] | Show / switch the agent backend for this conversation (reset clears the override). Switching clears the persisted session id. | | #default-backend [claude\|copilot] | Show / set / clear the global default agent backend (no arg clears it; falls back to config.claude.backend, default claude). |

Memory System (3-Layer)

  1. Per-conversation (CLAUDE.md in workspace dir) — auto-discovered by SDK, seeded on first use
  2. Global (~/.brainbotbeta/global/CLAUDE.md) — shared across all conversations
  3. Auto-memory — SDK-managed automatic pattern extraction

Plus history.md (auto-generated session summaries) and full transcript archives in conversations/.

BrainCore Integration (Local HTTP API)

The local-server exposes a REST endpoint for external tools (e.g. BrainCore) to proactively push messages to Teams channels.

Discovery: on startup, the local-server writes ~/.brainbotbeta/local-server.json:

{
  "host": "127.0.0.1",
  "port": 3978,
  "pid": 12345,
  "tokenPath": "C:\\Users\\you\\.brainbotbeta\\api-token"
}

Usage:

  1. Read ~/.brainbotbeta/local-server.json to discover host, port, and token path
  2. Read the file at tokenPath to get the bearer token
  3. Call the API:
curl -X POST http://127.0.0.1:3978/api/push \
  -H "Authorization: Bearer $(cat ~/.brainbotbeta/api-token)" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Hello from BrainCore!",
    "targetChannelId": "19:[email protected]",
    "conversationKey": "cloud_teams:oid:...",
    "threadId": "1781062301786",
    "sessionId": "uuid",
    "workspace": "D:\\my-project",
    "format": "markdown"
  }'

Response: 202 Accepted — message is queued in the send buffer, not confirmed delivered.

Security (3 layers):

  • Loopback-only: rejects non-127.0.0.1/::1 requests (403)
  • Bearer token: auto-generated at ~/.brainbotbeta/api-token (mode 0600), required via Authorization header (401)
  • Origin check: rejects requests with an Origin header to block browser CSRF (403)

Runtime Data

All runtime data lives under ~/.brainbotbeta/:

| Path | Purpose | |------|---------| | config/config.json | Gateway URL, MSAL params, team ID, claude/skills config | | config/state.json | Running daemon state (PID, uptime, auth status, server status) | | config/proxy-state.json | LLM proxy state (PID, port) | | auth/relay-token | Entra ID JWT for gateway auth (0o600) | | auth/graph-token | Graph API access token (0o600) | | auth/github-token | GitHub PAT for LLM proxy (0o600) | | auth/token_cache_*.json | MSAL token cache (0o600) | | api-token | Bearer token for local HTTP API (0o600, auto-generated) | | local-server.json | Discovery file for BrainCore: host, port, pid, tokenPath (0o600) | | data/brain.db | SQLite database | | global/CLAUDE.md | Global memory shared across conversations | | global/system-prompt.md | System prompt (user-editable) | | data/workspace/<key>/ | Per-conversation workspaces | | logs/ | Daemon + proxy logs (out.log, err.log) | | deploy/provision.json | Self-deploy output (app name, domain, client IDs) |

Development

Build

# Build everything (shared → CLI → bot-app → local-server)
npm run build

# Watch mode for CLI development
npm run dev

# Build individual packages
cd shared && npm run build
cd bot-app && npm run build
cd local-server && npm run build

Test

# Run local-server tests (vitest)
npm test

# Watch mode
cd local-server && npm run test:watch

# Coverage
cd local-server && npm run test:coverage

Packaging

# Build distributable .tgz (excludes bot-app, for end users)
npm run pack:dist

# Build deploy-kit .zip (bot-app + shared, for self-host)
npm run pack:deploy

Module Systems

| Package | Module | Target | Notes | |---------|--------|--------|-------| | shared | Dual CJS/ESM | ESNext | Conditional exports: import → ESM, require → CJS | | Root / CLI | CommonJS | ES2022 | | | bot-app | NodeNext (ESM) | ESNext | .js extensions in imports | | local-server | NodeNext (ESM) | ESNext | .js extensions in imports |

Design Principles

  1. 3-tier WebSocket relay: Cloud Gateway is a thin message router — no AI logic, no tunnel dependencies. Local-server connects outbound, works behind any NAT/firewall.

  2. Strict runtime isolation: bot-app and local-server share no code, no dependencies, and never import each other. Types flow through shared/ only.

  3. Owner-only by design: This is a personal assistant, not a multi-tenant service. Every message is validated against the owner's Entra OID.

  4. Defense in depth: Every hop is independently authenticated — Managed Identity for Bot Framework, JWT for WebSocket relay, OID validation for routing. Zero secrets stored.

  5. Local-first AI: All AI processing stays on your machine. The cloud Gateway is a thin message router. Your data, tools, and file system remain under your control.

  6. No silent failures: Every error must leave a trace. Empty catch {} blocks are not permitted.

  7. Session continuity: Both agent backends (Claude SDK and Copilot CLI) persist session IDs to SQLite so conversations survive server restarts. Offline messages are buffered by the Gateway and replayed on reconnect.

  8. Deterministic naming: Azure resource names are derived from the user's alias (brain-beta-{alias}), making them predictable and idempotent.

Uninstall

# 1. Stop the daemon
brainbotbeta stop

# 2. Stop the LLM proxy (if running)
brainbotbeta proxy stop

# 3. Remove the global CLI link
npm unlink -g brain

# 4. Delete all runtime data
rm -rf ~/.brainbotbeta

# 5. (Optional) Delete Azure resources created by deploy
az group delete --name <your-resource-group> --yes