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.28

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, powered by the Claude Agent SDK. One command connects to the shared gateway, another provisions your own — then you chat with Claude directly 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 · offline buffer"]
    end

    subgraph local ["User's Machine"]
        cli["brainbotbeta CLI\nMSAL auth · daemon\ntoken refresh · LLM proxy"]
        server["local-server\nClaude Agent SDK\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)" --> 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.

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
  • GitHub CLI (gh) — logged in with gh auth login (needed to download from private repo)
  • A Microsoft 365 account with Teams
  • A GitHub account (for Copilot LLM proxy authentication)

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 and reconnects.

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 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 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) |

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/             # Claude Agent SDK integration
│       │   ├── index.ts       # query() entry point, session resume, streaming
│       │   ├── context.ts     # Builds conversation context from DB history
│       │   ├── hooks.ts       # PreCompact hook: archive transcripts before compaction
│       │   └── system-prompt.ts  # System prompt loader
│       ├── 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 Claude Agent SDK, 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, 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 to local-server via WSS (or buffer if offline)
        ↓
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 → Claude Agent SDK
            → Load/resume session, build context, query Claude
            → Stream: typing indicators every 3s
            → 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

Local Server Features

Claude Agent SDK Integration

The local server processes every message through the Claude Agent SDK with:

  • 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

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 | Claude Agent SDK session IDs (for resume) | | 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 |

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/.

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) | | 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: Claude Agent SDK sessions survive server restarts via SQLite persistence. 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