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

meeglesdk

v0.2.3

Published

飞书项目 Open API TypeScript SDK

Readme

meeglesdk

TypeScript SDK for Feishu Project (Meego) Open API.

Runtime: Node.js 18+ (server-side only; browser usage is not supported).

Release notes: CHANGELOG.md Release protocol: RELEASE.md

Install

npm install meeglesdk

Quick Start

import { MeegoClient, withUserKey } from 'meeglesdk';

const client = new MeegoClient({
  pluginId: 'YOUR_PLUGIN_ID',
  pluginSecret: 'YOUR_PLUGIN_SECRET',
  baseURL: 'https://project.feishu.cn',
});

const items = await client.workItem.query(
  'project_key',
  'story',
  { work_item_ids: [123456] },
  withUserKey('user_key')
);

Client Options

const client = new MeegoClient({
  pluginId: 'YOUR_PLUGIN_ID',
  pluginSecret: 'YOUR_PLUGIN_SECRET',
  baseURL: 'https://project.feishu.cn', // optional
  timeout: 30000,                       // optional, ms
  retry: {
    maxRetries: 2,
    retryDelay: 1000,                   // base delay, exponential backoff
    maxRetryDelay: 30000,               // optional cap for backoff delay
    retryableErrorCodes: [10429, 50006] // optional override
  },
  rateLimit: 'meego-openapi',           // preset: doc-based limits (15 QPS + endpoint overrides)
  // or customize:
  // rateLimit: {
  //   enabled: true,                    // optional, default true when provided
  //   qps: 15,                          // per token QPS
  //   burst: 15,                        // optional burst size
  //   scope: 'token+method+path',       // 'token' | 'token+path' | 'token+method+path'
  //   entryTtlMs: 600000,               // optional, idle bucket cleanup (default 10 min)
  //   rules: [                          // optional overrides
  //     { method: 'POST', path: '/open_api/work_item/actual_time/update', qps: 10 },
  //   ],
  // },
  tokenType: 0,                         // 0: plugin_access_token, 1: virtual_plugin_token
  onTokenRefresh: (token, type, userKey) => {
    // persist token if needed
  },
  logger: console,                      // optional
});

Auth Flows

Plugin Token (Server)

const token = await client.auth.getPluginToken();

User Token (Auth Code Exchange)

const { token, expire_time, refresh_token } = await client.auth.getUserToken(code);

Manually Set User Token

client.setUserToken({
  userKey,
  token,
  expireTime: expireTimeSeconds,
  refreshToken,
  refreshTokenExpireTime,
  saasTenantKey: tenantKey,
});

Request-Level Auth (Recommended for Multi-User)

Do not store user tokens on a shared client. Instead, pass auth per request:

await client.workItem.query(
  'project_key',
  'story',
  { work_item_ids: [123456] },
  { auth: { type: 'user', token: userToken } }
);

// Helper shortcuts (return request options)
await client.workItem.query(
  'project_key',
  'story',
  { work_item_ids: [123456] },
  withUserKey('user_key')
);

Rules:

  • type: 'user': use the provided user token; userKey is not needed.
  • type: 'plugin': use plugin token; userKey is required for user-scoped APIs.
  • type: 'auto' (default): if userKey is provided and a cached token exists for that user, it is used; otherwise plugin token is used.
  • authMode (optional): only applies when a plugin token is used; controls x-auth-mode for strict read permission checks.

x-auth-mode (Force Read Auth for Plugin Token)

When calling read APIs with plugin_access_token (or virtual_plugin_token), you can set x-auth-mode to force the server to respect the X-User-Key permissions. This header has no effect when a user token is used.

  • authMode: 1 -> strict mode (recommended when you must enforce the user's read permissions)
  • authMode: 0 or omitted -> compatibility mode
await client.workItem.query(
  'project_key',
  'story',
  { work_item_ids: [123456] },
  { auth: { type: 'plugin', userKey: 'user_key', authMode: 1 } }
);

Token Management Notes

  • TokenManager caches plugin_access_token and refreshes it automatically.
  • user_access_token is cached per userKey when you call auth.getUserToken or client.setUserToken.
  • user_access_token will be auto-refreshed per user when possible (requires refresh_token).
  • Use request-level auth for multi-user apps; avoid sharing a client-scoped user token across users.

Timeout & Retry

  • Default timeout: 30s.
  • Retries use exponential backoff with a base delay and are enabled by default.
  • Network errors, timeouts, and retryable API error codes are retried (default: 10429, 50006).
  • Per-request overrides are supported via options.retry, options.skipRetry, and options.timeout.

AI Field

await client.aiField.update({
  ai_task_id: '7629005114472926736',
  field_value: 'Generated field value',
});

AI field webhooks use event types 8101 and 8102.

Work Item Field Options

const options = await client.workItem.getFieldOptions({
  project_key: 'project_key',
  work_item_type_key: 'story',
  work_item_id: 123456,
  field_key: 'priority',
});

Work Item Workflow Query

const workflow = await client.workItem.workflow.query(
  'project_key',
  'story',
  123456,
  {
    fields: ['priority', 'description'],
    flow_type: 0,
    expand: {
      need_user_detail: true,
      need_sub_task_parent: true,
      need_rich_text_mark_down: true,
      need_sub_workitem_detail: true,
    },
  },
  { auth: { type: 'plugin', userKey: 'user_key' } }
);

console.log(workflow.workflow_nodes);

Compound Field Values

await client.workItem.updateCompoundFieldValue({
  project_key: 'project_key',
  work_item_id: 123456,
  field_key: 'field_compound',
  action: 'add',
  fields: [[{ field_key: 'sub_field', field_value: 'value' }]],
});

Create Instance from Resource

const created = await client.config.resource.createInstanceFromResource(
  'project_key',
  872360000,
  {
    work_item_type_key: 'story',
    name: '校验必填创建实例',
    required_mode: 1,
    field_value_pairs: [
      { field_key: 'description', field_value: '测试描述' },
    ],
  },
  { auth: { type: 'plugin', userKey: 'user_key' } }
);

console.log(created.work_item_id);

WBS View (Query + Body)

const wbs = await client.workItem.workflow.getWbsView(
  'project_key',
  'story',
  123456,
  {
    query: { need_union_deliverable: true, need_schedule_table_agg: true },
    body: {
      expand: {
        need_union_deliverable: true,
        need_wbs_relation_chain_entity: true,
        need_wbs_relation_chain_path: true,
      },
    },
  },
  { auth: { type: 'plugin', userKey: 'user_key' } }
);

WBS Draft

WBS Draft is a standalone sub-service under workItem.wbsDraft. Do not call draft APIs from workItem.workflow.

const draft = await client.workItem.wbsDraft.createDraft({
  project_key: 'project_key',
  work_item_id: 123456,
  work_item_type_key: 'story',
});

await client.workItem.wbsDraft.patchDraft({
  project_key: 'project_key',
  work_item_id: 123456,
  draft_id: draft.draft_meta.draft_id,
  operation: {
    operation_type: 'UpdateName',
    operation_value: {
      update_name: {
        uuid: 'node_xxx',
        name: '更新后名称',
      },
    },
  },
});

const confs = await client.workItem.wbsDraft.getSubWorkItemConf({
  project_key: 'project_key',
  work_item_id: 123456,
  draft_id: draft.draft_meta.draft_id,
  node_uuid: 'node_xxx',
});

const createProgress = await client.workItem.wbsDraft.getCreateDraftProgress({
  project_key: 'project_key',
  work_item_id: 123456,
  draft_id: draft.draft_meta.draft_id,
});

const patchProgress = await client.workItem.wbsDraft.getPatchDraftProgress({
  project_key: 'project_key',
  transaction_id: 'tx_xxx',
});

const publishResult = await client.workItem.wbsDraft.publishDraft({
  project_key: 'project_key',
  work_item_id: 123456,
  draft_id: draft.draft_meta.draft_id,
});

const publishProgress = await client.workItem.wbsDraft.getPublishDraftProgress({
  project_key: 'project_key',
  work_item_id: 123456,
  draft_id: draft.draft_meta.draft_id,
  publish_order_id: publishResult.publish_order_id,
});

const resetResult = await client.workItem.wbsDraft.resetDraft({
  project_key: 'project_key',
  work_item_id: 123456,
  draft_id: draft.draft_meta.draft_id,
});

const resetProgress = await client.workItem.wbsDraft.getResetDraftProgress({
  project_key: 'project_key',
  work_item_id: 123456,
  draft_id: draft.draft_meta.draft_id,
});

const structure = await client.workItem.wbsDraft.queryStructure({
  project_key: 'project_key',
  work_item_id: 123456,
  draft_id: draft.draft_meta.draft_id,
});

const rowDetails = await client.workItem.wbsDraft.queryRowDetails({
  project_key: 'project_key',
  work_item_id: 123456,
  draft_id: draft.draft_meta.draft_id,
  uuids: ['row_xxx'],
  row_field_marks: ['meta.*', 'base.*'],
});

const rowConfig = await client.workItem.wbsDraft.queryRowConfig({
  project_key: 'project_key',
  work_item_id: 123456,
  draft_id: draft.draft_meta.draft_id,
  uuid: 'row_xxx',
});

WBS View

WBS View is a standalone sub-service under workItem.wbsView.

const structure = await client.workItem.wbsView.queryStructure({
  project_key: 'project_key',
  work_item_id: 123456,
});

const rowDetails = await client.workItem.wbsView.queryRowDetails({
  project_key: 'project_key',
  work_item_id: 123456,
  uuids: ['row_xxx'],
  row_field_marks: ['meta.*', 'base.schedule'],
});

const expandInfo = await client.workItem.wbsView.queryExpandInfo({
  project_key: 'project_key',
  work_item_id: 123456,
  work_item_type_key: 'story',
  expand: {
    need_parent_work_item: true,
    need_wbs_relation_chain_entity: true,
    need_wbs_relation_chain_path: true,
  },
});

Universal Search (Field Select)

const request = {
  project_key: 'project_key',
  work_item_type_key: 'story',
  search_group: {
    conjunction: 'AND',
    search_params: [{ param_key: 'created_at', value: Date.now() - 86400000, operator: '>' }],
  },
  field_selected: ['work_item_id', 'name'],
  pagination: { page_size: 20 },
};

const result = await client.workItem.search.universalSearch(request, {
  auth: { type: 'plugin', userKey: 'user_key' },
});

// Next page
const next = await client.workItem.search.universalSearch(
  {
    ...request,
    pagination: { page_size: 20, search_after: result.pagination.search_after },
  },
  { auth: { type: 'plugin', userKey: 'user_key' } }
);

Generic File Upload

const urls = await client.workItem.attachment.uploadFile(
  'project_key',
  { file: new Blob(['hello'], { type: 'text/plain' }), fileName: 'hello.txt' },
  { auth: { type: 'plugin', userKey: 'user_key' } }
);

const firstUrl = urls[0];

Release and OpenAPI Checks

# Build dist, verify npm pack contents, and compile package-name import examples.
bun run release:artifacts

# Compare selected OpenAPI request body fields with the TS SDK request types.
bun run openapi:field-diff

# Emit the same scan as JSON for CI/report consumers.
bun run openapi:field-report

# Refresh the local OpenAPI docs snapshot after intentionally updating Meego-API-docs.
bun run openapi:snapshot:update

# Verify the local OpenAPI docs mirror has not changed without a reviewed snapshot update.
bun run openapi:snapshot:check

# Validate the scan manifest before running the diff.
bun run openapi:field-targets:check

# Same diff scan, but exits non-zero when differences are found.
bun run openapi:field-check

# Verify response models keep response-only fields out of shared request structures.
bun run response-model-check

The OpenAPI docs snapshot pins the local Meego-API-docs markdown mirror by path, byte size, line count, and SHA-256 digest. Non-markdown files such as .DS_Store are ignored. When the mirror is intentionally refreshed, review SDK impact first, then run bun run openapi:snapshot:update and commit the snapshot diff.

The OpenAPI field scan reads the local Meego-API-docs mirror and reports the exact document path used for each target. It checks two source classes: endpoint request bodies from their API pages, and reusable SDK structures from Meego-API-docs/数据结构汇总.md. It currently covers 38 selected request bodies across work item, workflow, search, resource, review, attachment deletion, comment, subtask, work hour, batch update, compound-field update, field option, operation record, deliverable, and custom field APIs, plus shared structures such as expand params, field configs, field value pairs, role owners, schedules, search filters, relation details, transition-required info, work hour records, node schedule constraint rules, team options, number config, and user detail.

Add or remove scanned targets in scripts/openapi-field-targets.json; the checker reads that manifest instead of keeping endpoint config in code. bun run openapi:field-targets:check validates the manifest shape, referenced docs, structure headings, and SDK type declarations. bun run release:check runs openapi:snapshot:check and openapi:field-check, so npm publish and CI fail when the local OpenAPI mirror changes without a reviewed snapshot update or SDK fields drift from that mirror.

Shared request structures stay aligned to 数据结构汇总.md. Response-only additions use explicit response types such as FieldValuePairResponse, RoleOwnerResponse, and ScheduleResponse; endpoint-specific expand options use endpoint-specific types such as QueryWorkItemExpand and WorkflowQueryExpand. response-model-check keeps the main work item, workflow, resource, and WBS draft response models on those response-specific types.

Services Overview

Top-level services:

  • auth
  • workItem
  • space
  • user
  • config
  • view
  • measure
  • tenant
  • event
  • aiNode
  • aiField

workItem sub-services:

  • search, batch, comment, subtask, attachment, chat, workflow, wbsDraft, wbsView, workHour, review

Tests

Integration tests use real credentials in tests/config.ts.

bun test

Network access is required.

The resource detail success-path check is fixture-based because the public OpenAPI mirror does not expose a resource-instance delete endpoint. Set TEST_RESOURCE_WORK_ITEM_TYPE_KEY and TEST_RESOURCE_WORK_ITEM_ID to validate a real resource detail response without creating new resource instances.

Unit Tests (Edge Cases)

Unit tests mock network calls and focus on request parsing, retries, and token caching.

bun test tests/unit

Performance Benchmarks (Local)

Quick micro-benchmarks for request parsing and big-int handling (no network).

bun run tests/perf/benchmark.ts