Agent MessengerAgent Messenger
TypeScript SDK

Teams

TypeScript SDK reference for Microsoft Teams — client, credential management, and types.

Installation

npm install agent-messenger
import { TeamsClient, TeamsCredentialManager, TeamsError } from 'agent-messenger/teams'

TeamsClient

Work/school channel, directory and file methods use Microsoft Graph with the existing TeamsTokenProvider. Accounts without an auth login token (for example, set up with auth extract) fall back to the Teams CSA routes with the Skype token. Private/group chats continue to use the Skype messaging service. Tenant policies and permissions can restrict individual operations.

The main client for interacting with Microsoft Teams' API programmatically. Graph and Skype GET requests routed through the client request helpers include rate-limit handling and backoff on server errors; direct downloads are not retried. Search uses a POST and is not automatically retried. Mutations are attempted once to avoid duplicate delivery after an ambiguous response.

Skype token expiry is checked before Skype requests only when tokenExpiresAt is supplied or .login() loads stored credentials. .login() refreshes a stored device-code account; manually supplied SDK credentials remain a snapshot. For long-running SDK use, obtain fresh credentials at job boundaries. Graph/Substrate bearer tokens are minted and cached separately. If device-code refresh fails, reconnect explicitly with auth login; auth extract cannot repair Graph capabilities.

import { TeamsClient } from 'agent-messenger/teams'

const client = await new TeamsClient().login({ token })
// With expiry tracking: await new TeamsClient().login({ token, tokenExpiresAt: '2025-03-23T12:00:00Z' })

Or load saved credentials, with silent device-code refresh. Automatic extraction only occurs when no device-code account is stored:

import { TeamsClient } from 'agent-messenger/teams'

const client = await new TeamsClient().login()

Authentication

// Verify credentials and get identity
const user = await client.testAuth()
// → TeamsUser { id, displayName }

Teams

// Live Graph team names for a device-code work account
const teams = await client.listJoinedTeams()
// Legacy Skype conversation discovery: await client.listTeams()
// → TeamsTeam[]

// Get a specific team by ID
const team = await client.getTeam(teamId)
// → TeamsTeam { id, name, description? }

Channels

// List all channels in a team
const channels = await client.listChannels(teamId)
// → TeamsChannel[]

// Get a specific channel by ID
const channel = await client.getChannel(teamId, channelId)
// → TeamsChannel { id, team_id, name, type }

Channel IDs use a Teams thread ID format (19:xxx@thread.tacv2), not numeric snowflakes. Always pass the parent teamId alongside the channelId.

Messages

// Send a message to a channel
const msg = await client.sendMessage(teamId, channelId, 'Hello world')
// → TeamsMessage { id, channel_id, author, content, timestamp }

// Get recent messages from a channel (default limit: 50)
const messages = await client.getMessages(teamId, channelId)
const limited = await client.getMessages(teamId, channelId, 25)
// → TeamsMessage[]

// Get a single message by ID
const message = await client.getMessage(teamId, channelId, messageId)
// → TeamsMessage

// Get a single chat message by ID
const chatMessage = await client.getChatMessage(chatId, messageId)
// → TeamsMessage { id, channel_id, author, content, html?, files?, timestamp }

// Delete a message
await client.deleteMessage(teamId, channelId, messageId)

Reactions

// Add a reaction to a message
await client.addReaction(teamId, channelId, messageId, 'like')

// Remove your reaction
await client.removeReaction(teamId, channelId, messageId, 'like')

Users

// List all members in a team
const users = await client.listUsers(teamId)
// → TeamsUser[]

// Get a specific user by ID
const user = await client.getUser(userId)
// → TeamsUser { id, displayName, email?, userPrincipalName? }

Files

// Upload a file to a channel (by file path)
const file = await client.uploadFile(teamId, channelId, '/path/to/report.pdf')
// → TeamsFile { id, name, size, url, contentType? }

// List files in a channel
const files = await client.listFiles(teamId, channelId)
// → TeamsFile[]

// Download any URL from a message's `files` (inline image or attachment)
const { data, filename, contentType } = await client.downloadContent(message.files![0])

Messages read from chats, channels, and the real-time listener expose content (plain text), html (the original HTML of rich-text messages), and files (every downloadable URL: inline images first, in order, then attachments). downloadContent sends the Skype token only to Teams/Skype hosts and downloads SharePoint/OneDrive attachments through Microsoft Graph, which requires an auth login account.

TeamsCredentialManager

Manages Teams credentials stored at ~/.config/agent-messenger/teams-credentials.json. Files are written with 0o600 permissions. Supports multiple accounts (work and personal) with automatic migration from the legacy single-account format.

The static property TeamsCredentialManager.accountOverride lets you force a specific account type ('work' or 'personal') for all operations, overriding the stored current_account setting.

import { TeamsCredentialManager } from 'agent-messenger/teams'

const manager = new TeamsCredentialManager()
// Custom path: new TeamsCredentialManager('/custom/config/dir')
// Load full config from disk (returns null if file doesn't exist)
const config = await manager.loadConfig()
// → TeamsConfig | null

// Save full config to disk
await manager.saveConfig(config)

// Get stored token for the current account
const token = await manager.getToken()
// → string | null

// Get token with expiry info
const tokenInfo = await manager.getTokenWithExpiry()
// → { token: string; tokenExpiresAt?: string } | null

// Store a token for a specific account type
await manager.setToken('my-teams-token', 'work', '2025-03-23T12:00:00Z')

// Check if the current account's token has expired
const expired = await manager.isTokenExpired()
// → boolean

// Check if a specific account's token has expired
const workExpired = await manager.isAccountTokenExpired('work')
// → boolean

// Clear all stored credentials (deletes the file)
await manager.clearCredentials()

// Get current default team
const team = await manager.getCurrentTeam()
// → { team_id: string; team_name: string } | null

// Set the current default team
await manager.setCurrentTeam('team-uuid', 'Engineering')

// Get the active account type
const accountType = await manager.getCurrentAccountType()
// → 'work' | 'personal' | null

// Switch the active account
await manager.setCurrentAccount('personal')

// Get the full account object for the current account
const account = await manager.getCurrentAccount()
// → TeamsAccount | null

// Get all saved accounts
const accounts = await manager.getAccounts()
// → Record<string, TeamsAccount>

// Override account selection globally (static property)
TeamsCredentialManager.accountOverride = 'personal'

Types

import type {
  TeamsTeam,
  TeamsChannel,
  TeamsMessage,
  TeamsUser,
  TeamsReaction,
  TeamsFile,
  TeamsCredentials,
  TeamsAccountType,
  TeamsAccount,
  TeamsConfig,
  TeamsConfigLegacy,
  TeamsRealtimeMessage,
  TeamsMention,
  TeamsListenerEventMap,
  TeamsTrouterGenericEvent,
} from 'agent-messenger/teams'

The real-time listener emits TeamsRealtimeMessage:

interface TeamsRealtimeMessage {
  id: string
  chatId: string
  conversationType: 'chat' | 'channel'
  teamId?: string // set for channel messages
  channelId?: string // set for channel messages
  content: string // plain text
  html?: string // original HTML, rich-text messages only
  files?: string[] // downloadable URLs: inline images first, then attachments
  mentions: TeamsMention[] // always present; empty when no @mentions
  author: { id: string; displayName: string }
  messageType: string
  timestamp: string
}

interface TeamsMention {
  id: string // positional mention index
  mri?: string // Skype MRI of the mentioned user/tag
  displayName: string
}

These are type-only exports (no Zod schema — the listener constructs the event internally rather than parsing untrusted input).

Zod Schemas

Runtime-validated schemas are also exported for parsing API responses:

import {
  TeamsTeamSchema,
  TeamsChannelSchema,
  TeamsMessageSchema,
  TeamsUserSchema,
  TeamsReactionSchema,
  TeamsFileSchema,
  TeamsCredentialsSchema,
  TeamsAccountTypeSchema,
  TeamsAccountSchema,
  TeamsConfigSchema,
  TeamsConfigLegacySchema,
} from 'agent-messenger/teams'

Examples

Deploy Notifier

Post a deployment message, stream progress, and mark it complete with a reaction.

import { TeamsClient } from 'agent-messenger/teams'

const client = await new TeamsClient().login({ token, tokenExpiresAt })

// Post parent message to deploys channel
const msg = await client.sendMessage(teamId, channelId, '🚀 Deploying v2.1.0 to production...')
await client.addReaction(teamId, channelId, msg.id, 'like')

// Stream progress with follow-up messages
await client.sendMessage(teamId, channelId, '• Building application...')
await client.sendMessage(teamId, channelId, '• Running tests (142 passed)...')
await client.sendMessage(teamId, channelId, '• Rolling out to 3 regions...')

// Finalize
await client.removeReaction(teamId, channelId, msg.id, 'like')
await client.addReaction(teamId, channelId, msg.id, 'heart')
await client.deleteMessage(teamId, channelId, msg.id)
await client.sendMessage(teamId, channelId, '✅ v2.1.0 deployed to production')

Channel Monitor

List channels in a team and pull recent messages from each one.

import { TeamsClient } from 'agent-messenger/teams'

const client = await new TeamsClient().login({ token, tokenExpiresAt })

// List all channels in the team
const channels = await client.listChannels(teamId)

for (const channel of channels) {
  const messages = await client.getMessages(teamId, channel.id, 5)
  console.log(`#${channel.name} (${messages.length} recent messages)`)

  for (const msg of messages) {
    console.log(`  [${msg.author.displayName}] ${msg.content.slice(0, 80)}`)
  }
}

Team Summary

Build a snapshot-style overview of a team: members, channels, and latest activity.

import { TeamsClient } from 'agent-messenger/teams'

const client = await new TeamsClient().login({ token, tokenExpiresAt })

// Gather team info
const team = await client.getTeam(teamId)
const channels = await client.listChannels(teamId)
const members = await client.listUsers(teamId)

console.log(`Team: ${team.name}`)
console.log(`Members: ${members.length}`)
console.log(`Channels: ${channels.length}`)
console.log()

// Show latest message per channel
for (const channel of channels.slice(0, 5)) {
  const messages = await client.getMessages(teamId, channel.id, 1)
  const latest = messages[0]
  if (latest) {
    console.log(`#${channel.name}: "${latest.content.slice(0, 60)}" — ${latest.author.displayName}`)
  } else {
    console.log(`#${channel.name}: (no messages)`)
  }
}

File Pipeline

Collect files from one channel and re-upload them to an archive channel with a summary.

import { TeamsClient } from 'agent-messenger/teams'

const client = await new TeamsClient().login({ token, tokenExpiresAt })

// Get files from the source channel
const files = await client.listFiles(teamId, sourceChannelId)

for (const file of files.slice(0, 5)) {
  // Fetch the file content
  const response = await fetch(file.url)
  const buffer = Buffer.from(await response.arrayBuffer())

  // Write to a temp file and re-upload to archive channel
  const tmpPath = `/tmp/${file.name}`
  const fs = await import('node:fs/promises')
  await fs.writeFile(tmpPath, buffer)
  const uploaded = await client.uploadFile(teamId, archiveChannelId, tmpPath)

  await client.sendMessage(
    teamId,
    archiveChannelId,
    [
      `📎 Archived **${file.name}**`,
      `Size: ${(file.size / 1024).toFixed(0)} KB · Type: ${file.contentType ?? 'unknown'}`,
    ].join('\n'),
  )
}

Archive-ready chat pages

const page = await client.getChatMessagesPage(chatId, 50)
// page.messages retains raw_content, content_type, mentions, attachments and raw.
const next = page.next_cursor ? await client.getChatMessagesPage(chatId, 50, page.next_cursor) : undefined

getChatMessages(chatId, limit, { includeSystem }) follows pages automatically and includes media records; system records are included only with includeSystem: true. getMessages and getThreadReplies follow Graph pages up to the requested limit. Original server bodies remain in raw, even when text is rendered for agents. Search results are indexed previews, with their original result in raw.

Reply targets and reaction permissions

const reply = await client.getMessage(teamId, channelId, replyId, rootId)
await client.addReaction(teamId, channelId, replyId, '👍', rootId)
await client.removeReaction(teamId, channelId, replyId, 'like', rootId)
await client.deleteMessage(teamId, channelId, replyId, rootId)

The optional final root ID selects the nested reply route. Unicode reactions pass through; the legacy names like, heart, laugh, surprised, sad, angry convert to Unicode. Channel reactions require ChannelMessage.Send. Deletion first resolves the exact Graph target, then uses the existing Skype token for Chat Service. It does not add Graph ChannelMessage.ReadWrite consent. Teams ownership and deletion policy still apply; delete writes are attempted once without switching APIs on failure. Own chat edits accept the empty HTTP 200 response returned by Skype; always use readback to verify the changed message.

On this page