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

multi-openim-channel

v0.2.0

Published

Multi-account / multi-server OpenIM channel plugin for OpenClaw. Pure IM transport (no business decision logic). Config-driven token refresh, optional friend-API guard, dedup + per-account exponential backoff on recovery.

Readme

multi-openim-channel

An OpenClaw gateway plugin for connecting to OpenIM servers (via @openim/client-sdk) with multi-account / multi-server support, config-driven token refresh, and an optional friend-API guard.

Highlights

  • Multi-account, multi-server out of the box. Each accounts.<id> has its own wsAddr / apiAddr / token, so a single plugin instance can hold long-lived connections to several different OpenIM servers in parallel.
  • No silent default account. The single-account fallback (top-level token / wsAddr / apiAddr on the channel block) is intentionally removed. Named accounts under accounts.<id> are mandatory.
  • Host-canonical session keys. Direct + group inbounds always reuse whatever resolveAgentRoute returns (which respects session.dmScope, agents.bindings, and identityLinks), so plugin / host store / startup-migration all agree on the same key string. Previously the plugin emitted a bare key (e.g. multi-openim:<peer>:primary) that the host's canonicalization pass rewrote with an agent:main: prefix, producing two stored sessions for the same logical conversation and visibly cross-talking across peers. Fixed in 0.1.3. The plugin still passes peer to the host so unresolved peers don't collapse onto agent:main:main (the 0.1.2 fix).
  • Optional friend SDK guard. When disableFriendSdk is true (the default), the OpenIM JS SDK's friend methods (addFriend / acceptFriendApplication / ...) are stubbed with throwing functions that surface a grep-able error code (MULTI_OPENIM_FRIEND_API_DISABLED). Use this when an external system owns friend relationships; set disableFriendSdk: false to restore raw SDK behavior.
  • Config-driven HTTP token refresh. When the SDK emits OnUserTokenExpired / OnUserTokenInvalid / OnConnectFailed / OnKickedOffline, the plugin performs an in-process fetch against an operator-supplied refresh endpoint, parses the response by configurable dot-paths, optionally writes fields back into a sidecar JSON state file, and patches openclaw.json so the new token survives a restart. The channel does not embed any backend-specific strings — every backend detail is operator input. No subprocesses, no bridge plugin. A globalThis.__multiOpenimTokenRefresher hook is still supported for power users who need imperative recovery. On terminal failure a per-account manual-login marker JSON is written.
  • Strict accountId in tools. MCP tools (multi_openim_send_text / multi_openim_send_image / multi_openim_send_video / multi_openim_send_file) require accountId; no "first connected wins" surprises.
  • Per-channel health-monitor interval. healthCheckIntervalMinutes (default 30) overrides the gateway global without an unrelated gateway.channelHealthCheckMinutes knob.

Install

# from a local checkout (recommended while iterating):
openclaw plugins install /path/to/multi-openim-channel

# from the npm registry (when published):
openclaw plugins install multi-openim-channel

After install:

openclaw multi-openim setup       # interactive (writes channels.multi-openim.accounts.primary)
openclaw gateway restart          # load the channel
openclaw plugins inspect multi-openim-channel --json

Identity mapping

  • npm package: multi-openim-channel
  • plugin id: multi-openim-channel
  • channel id: multi-openim (used in openclaw.json under channels.multi-openim)
  • setup command: openclaw multi-openim setup [--account <id>]
  • MCP tools: multi_openim_send_text, multi_openim_send_image, multi_openim_send_video, multi_openim_send_file

Configuration (~/.openclaw/openclaw.json)

{
  "channels": {
    "multi-openim": {
      "enabled": true,
      "healthCheckIntervalMinutes": 30,
      "disableFriendSdk": true,
      "tokenRefresh": {
        "mode": "http",
        "http": {
          "stateFile": "/abs/path/to/tokens.json",
          "endpoint":  "https://auth.example.com/refresh",
          "headers":   { "content-type": "application/json" },
          "body":      { "refreshToken": "{state.refresh_token}" },
          "responseTokenPath": "token"
        }
      },
      "accounts": {
        "primary": {
          "enabled": true,
          "token":   "<JWT for OpenIM server A>",
          "wsAddr":  "ws://im-a.example.com:10001",
          "apiAddr": "http://im-a.example.com:10002"
        },
        "team-b": {
          "enabled": true,
          "token":   "<JWT for OpenIM server B>",
          "wsAddr":  "ws://im-b.example.com:10001",
          "apiAddr": "http://im-b.example.com:10002",
          "requireMention": true,
          "inboundWhitelist": []
        }
      }
    }
  }
}

userID / platformID are optional and auto-derived from JWT UserID / PlatformID claims when omitted.

See SCHEMA.md for the full field reference.

Token refresh

Three modes, selected by channels.multi-openim.tokenRefresh.mode:

| Mode | Behavior | |---|---| | http (recommended) | Pure in-process fetch driven by tokenRefresh.http. Reads optional context from a stateFile, substitutes {accountId} / {state.<field>} placeholders into the configured endpoint / headers / body, posts the request, extracts the new token by dot-path, optionally writes-back response fields into the state file, and patches openclaw.json. | | hook (default for back-compat) | Call globalThis.__multiOpenimTokenRefresher(accountId, reason). If unset, or it returns null / throws, write the manual-login marker. | | off | Don't try to recover — write the manual-login marker immediately. |

Manual-login marker default path template: ~/.openclaw/multi-openim/manual-login.json. The configured value is treated as a template — the accountId is inserted before the extension, so the actual files written are ~/.openclaw/multi-openim/manual-login.<accountId>.json. This guarantees N accounts produce N independent markers instead of silently overwriting each other. Override via tokenRefresh.manualLoginMarkerPath.

tokenRefresh.http (config-driven HTTP refresh)

The channel does not know your auth backend. Every backend-specific string — the URL, the request body shape, the JSON paths to find the new token — is configuration. {accountId} and {state.<field>} are substituted into string templates (URL, header values, body string leaves, clearOnSuccess paths). state.<field> reads stateFile[accountId][field].

Minimal example, against an auth server that exposes POST /refresh with a {refreshToken} body and a {token: ...} response:

{
  "channels": {
    "multi-openim": {
      "tokenRefresh": {
        "mode": "http",
        "http": {
          "stateFile": "/abs/path/to/tokens.json",
          "endpoint":  "https://auth.example.com/refresh",
          "method":    "POST",
          "headers":   { "content-type": "application/json" },
          "body":      { "refreshToken": "{state.refresh_token}" },
          "responseTokenPath":    "token",
          "stateWriteBack":       { "refresh_token": "refresh_token" }
        }
      }
    }
  }
}

The expected tokens.json shape is keyed by accountId:

{
  "primary": { "refresh_token": "..." },
  "team-b":  { "refresh_token": "..." }
}

Multi-server example (different auth backend per account, response has the new token under tokens.openimToken with a data.openimToken fallback, and three response fields are persisted back into state):

{
  "tokenRefresh": {
    "mode": "http",
    "http": {
      "stateFile":  "/abs/path/to/tokens.json",
      "endpoint":   "{state.auth_server_url}/api/refresh",
      "method":     "POST",
      "headers":    { "content-type": "application/json" },
      "body":       { "refreshToken": "{state.refresh_token}" },
      "timeoutMs":  15000,
      "responseTokenPath":  ["tokens.openimToken", "data.openimToken"],
      "stateWriteBack": {
        "openim_token":  ["tokens.openimToken",  "data.openimToken"],
        "access_token":  ["tokens.accessToken",  "data.accessToken"],
        "refresh_token": ["tokens.refreshToken", "data.refreshToken"]
      },
      "clearOnSuccess": ["/abs/path/to/manual-login.{accountId}.json"]
    }
  }
}

On a successful refresh the new token is also written to channels.multi-openim.accounts.<accountId>.token in ~/.openclaw/openclaw.json (or $OPENCLAW_CONFIG_PATH) so it survives a gateway restart.

globalThis.__multiOpenimTokenRefresher (programmatic hook)

Available for mode: "hook". Use this when the refresh logic is too imperative for HTTP-template config:

globalThis.__multiOpenimTokenRefresher = async (accountId, reason) => {
  // reason looks like "OnUserTokenExpired" or "OnConnectFailed errCode=10001"
  const newToken = await yourTokenStore.refresh(accountId, reason);
  if (!newToken) return null;
  return { token: newToken };        // optionally include `userID` if your store rotates identity
};

Friend-guard (default ON)

MULTI_OPENIM_FRIEND_API_DISABLED: SDK addFriend() is disabled by multi-openim-channel
(account=primary). Route friend operations through your own backend.

The following SDK methods are replaced with throwing stubs on every connected account: addFriend, addFriendV2, applyFriend, acceptFriendApplication, refuseFriendApplication, deleteFriend, checkFriend, setFriendRemark, addBlack, removeBlack, getFriendApplicationListAsApplicant, getFriendApplicationListAsRecipient, getFriendApplicationList, getRecvFriendApplicationList, getSendFriendApplicationList, getFriendList.

Set disableFriendSdk: false on the channel block (or per account) to restore raw SDK behavior. Recommended when an external system is the authoritative store for friend relationships and you want a single source of truth.

Development

npm install
npm run build
npm run validate    # static checks on dist + manifest
npm run smoke       # optional live SDK login test (needs .env)

For npm run smoke, copy .env.example to .env and fill in test credentials.

License

MIT. See LICENSE.