Teams
TypeScript SDK reference for Microsoft Teams — client, credential management, and types.
Installation
npm install agent-messengerimport { 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) : undefinedgetChatMessages(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.