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

connect-feishu-bot

v0.3.0

Published

Full-featured Feishu/Lark bot SDK. QR setup, CardKit streaming, emoji reactions, messaging, mentions. 飞书机器人 SDK:扫码创建、流式卡片、表情反应、消息收发、@提及。

Readme

connect-feishu-bot

Create and stream with Feishu/Lark bots. QR code setup + CardKit streaming cards.

扫码创建飞书机器人 + 流式卡片响应。

Before vs After

| | Before | After | |--|--------|-------| | Steps | 9 (create app on FOP, configure 5 permissions, publish, wait for admin approval, enter credentials, configure event subscriptions, add events, publish again, wait for admin approval again) | 1 (scan QR code) | | FOP operations | Manual throughout | None | | Admin approvals | 2 rounds | 0 | | Time | 15-30 minutes | < 1 minute |

Install

# Run directly (no install needed)
npx connect-feishu-bot

# Or install globally
npm install -g connect-feishu-bot

# Or add to your project
npm install connect-feishu-bot

CLI Usage

Create a new bot

npx connect-feishu-bot

A QR code will appear in your terminal. Scan it with the Feishu mobile app. That's it.

$ npx connect-feishu-bot

  Scan with Feishu to create your bot (请使用飞书扫码,创建机器人):

  ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
  █ ▄▄▄▄▄ █ ▄▄ █ ▄▄▄ █
  █ █   █ █▄█ ▄█ █▄█ █
  █ █▄▄▄█ █ ▄▄▄█▄▄▄▄ █
  ...

  Waiting for scan...

  ✓ Bot created! (机器人创建成功!)

  App ID:     cli_a93xxxxxxxxxxxx
  App Secret: ********************************

  Done! Your bot is ready to use.

JSON output

For scripting or piping into other tools:

npx connect-feishu-bot --json
{
  "appId": "cli_a93xxxxxxxxxxxx",
  "appSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "userOpenId": "ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "domain": "feishu"
}

Validate existing credentials

npx connect-feishu-bot --validate cli_a93xxxxxxxxxxxx:your_app_secret

Options

| Flag | Description | |------|-------------| | --json | Output result as JSON | | --validate <appId:appSecret> | Validate existing credentials | | --timeout <seconds> | QR code expiry timeout (default: 600) | | --verbose | Show QR code URL in addition to QR code | | --help | Show help | | --version | Show version |

Programmatic API

connectFeishuBot(options?): Promise<ConnectResult>

Create a new Feishu bot via QR code scanning.

import { connectFeishuBot } from 'connect-feishu-bot'

const result = await connectFeishuBot({
  onQRCode: (url) => {
    // Display the QR code in your UI
    console.log('Scan this URL:', url)
  },
  onStatus: (status) => {
    switch (status.phase) {
      case 'waiting_for_scan':
        console.log('Please scan the QR code...')
        break
      case 'success':
        console.log('Bot created:', status.appId)
        break
      case 'expired':
        console.log('QR code expired')
        break
    }
  },
})

console.log(result.appId)     // "cli_a93xxxxxxxxxxxx"
console.log(result.appSecret) // "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
console.log(result.domain)    // "feishu" or "lark"

Options

interface ConnectOptions {
  /** Called when the QR code URL is ready. Display it to the user. */
  onQRCode?: (url: string) => void

  /** Called on status changes during the registration flow. */
  onStatus?: (status: RegistrationStatus) => void

  /** AbortSignal to cancel the registration flow. */
  signal?: AbortSignal

  /** API environment. Default: 'prod' */
  env?: 'prod' | 'boe' | 'pre'
}

Return value

interface ConnectResult {
  /** The App ID of the newly created bot (e.g. "cli_a93xxxxxxxxxxxx") */
  appId: string

  /** The App Secret of the newly created bot */
  appSecret: string

  /** The open_id of the user who scanned the QR code */
  userOpenId?: string

  /** Whether this is a Feishu (China) or Lark (International) bot */
  domain: 'feishu' | 'lark'
}

Status events

type RegistrationStatus =
  | { phase: 'initializing' }
  | { phase: 'waiting_for_scan'; qrUrl: string; expiresIn: number }
  | { phase: 'success'; appId: string; appSecret: string; userOpenId?: string; domain: 'feishu' | 'lark' }
  | { phase: 'denied' }
  | { phase: 'expired' }
  | { phase: 'error'; message: string }

validateCredentials(appId, appSecret): Promise<boolean>

Check whether an App ID and App Secret pair is valid.

import { validateCredentials } from 'connect-feishu-bot'

const valid = await validateCredentials('cli_a93xxxxxxxxxxxx', 'your_app_secret')
console.log(valid) // true or false

Cancellation

Use an AbortSignal to cancel the QR code flow:

const controller = new AbortController()

// Cancel after 60 seconds
setTimeout(() => controller.abort(), 60_000)

try {
  const result = await connectFeishuBot({
    signal: controller.signal,
    onQRCode: (url) => renderQR(url),
  })
} catch (err) {
  if (err.name === 'AbortError') {
    console.log('Cancelled by user')
  }
}

Streaming Cards

Real-time streaming responses using Feishu's CardKit 2.0. Users see text appear character-by-character with a typewriter animation — no more dead silence while your bot thinks.

流式卡片响应。用户实时看到文字逐字生成,像 ChatGPT 一样丝滑。

Quick start

import * as lark from '@larksuiteoapi/node-sdk'
import { createStreaming } from 'connect-feishu-bot'

const client = new lark.Client({ appId: '...', appSecret: '...' })
const streaming = createStreaming(client)

// First call — card appears with "Thinking..." and a loading icon
await streaming.update(chatId, sessionId, '')

// Stream text — typewriter animation at 100ms intervals
await streaming.update(chatId, sessionId, 'Let me think about that...')

// Finalize — loading icon removed, optional elapsed time footer
await streaming.complete(chatId, sessionId, { elapsed: 2400 })

API

createStreaming(client): StreamingManager

Create a streaming manager from a @larksuiteoapi/node-sdk Client instance.

manager.update(chatId, sessionId, text, options?)

Send or update a streaming card. First call creates the card, subsequent calls stream text.

  • sessionId — any number or string identifying the streaming session
  • options.replyTo — reply to a specific message ID
  • options.replyInThread — reply within a thread

manager.complete(chatId, sessionId, options?)

Finalize the card. Closes streaming mode, shows final text.

  • options.elapsed — elapsed time in ms, shown in the footer

manager.abort(chatId, sessionId)

Abort the card. Closes streaming with partial text preserved.

manager.dispose()

Abort all active sessions and release resources.

How it works

Uses Feishu's CardKit 2.0 API for native streaming with typewriter animation:

update() first call  →  cardkit.v1.card.create (streaming_mode: true)
                     →  im.message.create (card_id reference)

update() subsequent  →  cardkit.v1.cardElement.content (100ms throttle)

complete()           →  cardkit.v1.card.settings (streaming_mode: false)
                     →  cardkit.v1.card.update (final card)

If CardKit is unavailable (missing permissions), automatically falls back to im.message.patch at 1500ms throttle. The API stays the same — your code doesn't need to change.

Integration with AI agents

// In your Feishu channel implementation:
const streaming = createStreaming(larkClient)

// Called by the agent framework during response generation
async function sendMessageDraft(chatId: string, draftId: number, text: string) {
  await streaming.update(chatId, draftId, text)
}

// After response is complete
await streaming.complete(chatId, draftId, { elapsed: responseTime })

Reactions

Content-aware emoji reactions on incoming messages. Acknowledges instantly, escalates with time, clears when the response arrives.

import { createReactions } from 'connect-feishu-bot'

const reactions = createReactions(client)

// On incoming message — instant, content-aware reaction
await reactions.acknowledge(chatId, messageId, text, { hasAttachment: true })

// When bot starts responding — reaction steps aside
await reactions.clear(chatId)

// On disconnect — clean up timers
reactions.dispose()

Emoji palette

| Trigger | Emoji | Meaning | |---------|-------|---------| | Greeting / affirmative | FINGERHEART 🫰 | Warm acknowledgment | | Question | THINKING 🤔 | Contemplating | | Help request | OnIt 🫡 | On it | | Gratitude | HEART ❤️ | Thanks received | | Attachment / long text | StatusReading 📖 | Reading | | 5s no response | Typing ⌨️ | Still processing | | 15s no response | STRIVE 💪 | Deep work |

Race-condition handling

  • Rapid messages: old reaction is cleared before new one is added
  • clear() during in-flight acknowledge(): generation counter prevents orphaned reactions
  • Escalation: timers are chained (not parallel) to prevent duplicate reactions

Integration Examples

Electron app

Show the QR code in a BrowserWindow:

import { connectFeishuBot } from 'connect-feishu-bot'
import QRCode from 'qrcode' // or any QR renderer

ipcMain.handle('feishu:connect', async () => {
  const result = await connectFeishuBot({
    onQRCode: (url) => {
      // Send QR code URL to renderer for display
      mainWindow.webContents.send('feishu:qrcode', url)
    },
    onStatus: (status) => {
      mainWindow.webContents.send('feishu:status', status)
    },
  })
  return result
})

React component

import { connectFeishuBot } from 'connect-feishu-bot'
import { QRCodeSVG } from 'qrcode.react'

function FeishuConnect() {
  const [qrUrl, setQrUrl] = useState<string>()
  const [status, setStatus] = useState<string>('idle')

  const connect = async () => {
    const result = await connectFeishuBot({
      onQRCode: setQrUrl,
      onStatus: (s) => setStatus(s.phase),
    })
    // Use result.appId and result.appSecret
  }

  return (
    <div>
      <button onClick={connect}>Connect Feishu</button>
      {qrUrl && <QRCodeSVG value={qrUrl} />}
      <p>Status: {status}</p>
    </div>
  )
}

Claude Code / AI agent

npx connect-feishu-bot --json | jq '.appId'

Or in a setup script:

#!/bin/bash
RESULT=$(npx connect-feishu-bot --json)
export FEISHU_APP_ID=$(echo "$RESULT" | jq -r '.appId')
export FEISHU_APP_SECRET=$(echo "$RESULT" | jq -r '.appSecret')
echo "Feishu bot connected: $FEISHU_APP_ID"

How It Works

This package uses Feishu's device-code-style app registration API:

POST https://accounts.feishu.cn/oauth/v1/app/registration

Step 1: action=init
  → Returns supported auth methods

Step 2: action=begin, archetype=PersonalAgent
  → Returns QR code URL + device_code

Step 3: action=poll, device_code=xxx (repeated)
  → Returns appId + appSecret on scan

The archetype=PersonalAgent creates a lightweight bot that comes pre-configured with the necessary permissions. No manual setup on the Feishu Open Platform is required.

Feishu (China) and Lark (International) are both supported. The domain is auto-detected based on the scanning user's tenant.

Requirements

  • Node.js >= 18 (uses native fetch)
  • A Feishu or Lark account with the mobile app installed

Security

  • No credentials are stored by this package. The caller decides how to persist them.
  • App Secrets are never logged. The CLI masks them with * characters.
  • validateCredentials only returns boolean. No token or secret is exposed.

Feishu vs Lark

| | Feishu (飞书) | Lark | |--|--------------|------| | Region | China | International | | API endpoint | accounts.feishu.cn | accounts.larksuite.com | | Auto-detected | Yes | Yes |

Both are fully supported. The domain is detected automatically when the user scans the QR code.

Troubleshooting

QR code not scanning

  • Ensure you're using the Feishu mobile app (not a generic QR scanner)
  • Check your network connection
  • Try --verbose to get the raw URL and open it manually

"authorization_pending" timeout

The QR code expires after 10 minutes by default. Re-run the command to get a fresh code.

Credentials validation fails

  • Double-check the App ID starts with cli_
  • Ensure the App Secret has no extra whitespace
  • The bot may have been deleted on Feishu's side

License

MIT