Skip to content

fullstackjam/lark-coding-agent-bridge

 
 

Repository files navigation

@fullstackjam/lark-coding-agent-bridge

Private fork of zarazhangrui/lark-coding-agent-bridge (upstream npm package: lark-channel-bridge). The fork's reason for existing is to add an opencode adapter alongside the upstream's Claude Code and Codex adapters. Most documentation below is upstream's; fork-specific notes are flagged inline.

A lightweight bot that bridges Feishu / Lark messenger with your local Claude Code, Codex, or opencode CLI. Run one command, scan a QR code to bind a PersonalAgent app, and talk to your local coding agent from chat.

中文 README

For a product walkthrough, see the Feishu document.

What it does

  • Forwards Feishu / Lark messages to local Claude Code, Codex, or opencode CLI. Send a DM directly, or @bot in a group.
  • Streaming card: text replies and tool calls update on one Lark card in real time.
  • COT process messages: optionally send a process message with agent progress text and tool calls, then send the final answer separately.
  • Session continuity: each chat, topic, or document comment thread keeps its own session.
  • Queueing and batching: messages sent in quick succession are handled together; messages sent during a run are queued for the next turn, while commands like /new, /cd, /ws use, and /stop can interrupt the current task.
  • Multiple workspaces: use /cd to switch the current project, and /ws to save and reuse common project directories.
  • Images and files: send them to the bot directly, and the bridge downloads them locally for the agent.
  • Interactive cards: /help, /ws list, and /status return cards with clickable buttons.

What this fork adds over upstream

Upstream already covers Claude Code and Codex thoroughly. This fork primarily adds the opencode lane and a few cross-agent UX improvements built around it:

Addition Why it matters Applies to
opencode adapter Upstream supports Claude / Codex only; this fork adds opencode as the third agent, reusing all upstream features (profiles, workspaces, permission modes, cards). opencode profile
Persistent opencode sessions Multiple user turns in one chat reuse the same opencode session so the agent remembers context. Without this every message would mint a new session and the agent would forget prior turns. opencode
Background-task wake-up card When the oh-my-openagent plugin's background task finishes, it can proactively post a fresh streaming card in Lark to continue the conversation — you don't have to ask again. opencode + oh-my-openagent
Interactive permission card When opencode emits permission.asked, the bridge surfaces it as a card with Allow once / Always allow / Reject buttons, with a 5-minute timeout that auto-rejects. opencode
/new chat <name> spawns a group One command creates a new Lark group with the bot as owner and registers the requester as the workbench owner. Messages in that group don't need @bot — same idea as lark-opencode-bridge's /spawn. all agents
Better tool-call rendering Tool cards read opencode's state.title: ✅ read — src/foo.ts instead of bare ✅ read. Case-insensitive switch + camelCase fallbacks (filePath) + opencode-native names (todowrite, ast_grep_search). all agents, opencode benefits most

Data flow

flowchart LR
    User([Feishu / Lark user])
    Lark[Feishu WS<br/>persistent connection]
    Bridge[lark-channel-bridge<br/>local daemon]
    Agent[Claude / Codex / opencode<br/>local CLI]

    User -- message / image --> Lark
    Lark -- pushed event --> Bridge
    Bridge -- prompt / stdin --> Agent
    Agent -- streaming events --> Bridge
    Bridge -- streaming card --> Lark
    Lark -- render card --> User
Loading

Code and model execution stay on your machine; Lark only carries messages and renders cards.

Persistent opencode sessions + background-task wake-up

sequenceDiagram
    autonumber
    participant U as User
    participant B as Bridge
    participant O as opencode serve
    participant P as oh-my-openagent

    U->>B: "look up X, take your time"
    B->>O: createSession + prompt_async
    O-->>B: SSE: text / tool / status:idle
    B->>U: card (turn 1 done)

    Note over O,P: background task running<br/>(LSP indexing / long query)

    P->>O: notifyParentSession<br/>(prompt_async injects synthetic user msg)
    O-->>B: SSE: new message.updated
    B->>U: new card<br/>🔔 background task complete — agent continues
Loading

Notes:

  • Each chat runs one long-lived SSE consumer; the opencode session is reused across user turns until /new or /cd resets it.
  • Wake-up detection works by tracking SSE messageIDs — trailing re-broadcasts from the just-ended turn are deduped, so the wake-up card only fires for an actually new turn.
  • The wake-up card goes through the same processAgentStream + channel.stream pipeline as user-initiated turns, so streaming text, tool cards, permission prompts, and the signed stop button all work identically.

/new chat: spawn a working group

Inspired by lark-opencode-bridge's /spawn. One group = one session = one project context.

flowchart TD
    A["/new chat workbench"] --> B[bridge calls<br/>lark im.chat.create]
    B --> C[new group: bot is owner<br/>requester invited]
    C --> D[workbenchGroups<br/>chatId → requester openId]
    D --> E[requester messages in the group<br/>**no @ needed**]
    E --> F{shouldTriggerWorkbenchMessage}
    F -- senderId == owner --> G[run triggers]
    F -- @ another user --> H[skip]
    F -- reply to non-bot msg --> H
Loading

How it works:

  1. /new chat <name> creates the Lark group with the bridge as owner and invites the requester.
  2. cwd inherits from the originating chat's workspace (anything you set via /cd carries over).
  3. chatId → requester openId is persisted to workbenchGroups in config.json.
  4. Any non-@, non-cross-replied message from the requester in that group triggers a run; messages that @ someone else or reply to a non-bot message are skipped.

⚠️ Requires the Lark "receive all group messages" scope: by default Lark only delivers @bot-mention events to the app. For "owner without @" to actually fire, add the im:message.group_msg:readonly permission ("Receive group messages") in the Lark Developer Console and get it approved. This is an advanced scope — without it, the bridge logic is correct but no event ever arrives. Verify by sending a non-@ message in the workbench group and checking ~/.lark-channel/profiles/<profile>/logs/bridge-$(date +%Y%m%d).jsonl for a matching intake.enter line; if there isn't one, Lark didn't push it.

Prerequisites

Install

npm i -g @fullstackjam/lark-coding-agent-bridge
# or
pnpm add -g @fullstackjam/lark-coding-agent-bridge

The installed CLI command is still lark-channel-bridge for upstream compatibility.

First run

lark-channel-bridge run

The first run opens a QR-code wizard:

  1. A QR code renders in your terminal.
  2. Scan it with the Feishu / Lark app.
  3. Pick or create a PersonalAgent app.
  4. If prompted, choose which agent to initialize.
  5. Config is written to ~/.lark-channel/config.json.

You do not need to choose a project directory up front. The bridge creates a profile-managed default working directory; after startup, send /cd <path> in Feishu / Lark to switch to a real project.

If you already have a PersonalAgent app, pass --app-id during initialization to skip app creation. The command prompts for the App Secret.

lark-channel-bridge run --app-id cli_xxx
# or initialize and start the background service directly
lark-channel-bridge start --app-id cli_xxx

For Lark global apps, add --tenant lark.

Background service

Use run for first-run setup and foreground debugging. After the bot can send and receive messages, stop the foreground process with Ctrl-C, then use an OS-managed service for background operation:

lark-channel-bridge start
lark-channel-bridge status
lark-channel-bridge stop

Install globally before using service commands. The daemon's launchd plist / systemd unit / Windows task records the bridge CLI path; if that path comes from an npm temp cache through npx, the daemon can break when the cache is cleaned. run is fine through npx as a one-shot foreground process.

Service commands install a per-profile service:

lark-channel-bridge start [--profile <name>]
lark-channel-bridge stop [--profile <name>]
lark-channel-bridge restart [--profile <name>]
lark-channel-bridge status [--profile <name>]
lark-channel-bridge unregister [--profile <name>]

Platform mapping:

  • macOS: launchd user agent ai.lark-channel-bridge.bot.<profile>
  • Linux: systemd user unit lark-channel-bridge.bot.<profile>.service
  • Windows: Task Scheduler task LarkChannelBridge.Bot.<profile>, launched through a .cmd wrapper

Daemon logs are under ~/.lark-channel/profiles/<profile>/logs/daemon/.

Multiple profiles: Claude, Codex, and opencode

By default, the bridge starts with the currently selected profile. Use profile use <name> to change it. Each profile keeps its own app credentials, sessions, working directories, and logs. Create multiple profiles only when you need to connect multiple PersonalAgent apps, or run Claude, Codex, and opencode as separate bots:

lark-channel-bridge start --profile claude --agent claude
lark-channel-bridge start --profile codex --agent codex
lark-channel-bridge start --profile opencode --agent opencode

For example, to restart only the Codex bot:

lark-channel-bridge restart --profile codex
lark-channel-bridge status --profile codex

Commands

Host CLI

lark-channel-bridge run [--profile <name>] [--agent claude|codex|opencode] [--workspace <path>] [-c <config>]
lark-channel-bridge migrate [--profile <name>] [--agent claude|codex|opencode]
lark-channel-bridge ps
lark-channel-bridge kill <id|#>
lark-channel-bridge --help

profile use <name> changes the profile used by later default starts. Use these profile management commands when running separate Claude / Codex / opencode bots, connecting multiple PersonalAgent apps, or doing scripted deployment:

lark-channel-bridge profile create claude --agent claude
lark-channel-bridge profile create codex --agent codex
lark-channel-bridge profile create opencode --agent opencode
lark-channel-bridge profile list
lark-channel-bridge profile use <name>
lark-channel-bridge profile remove <name>
lark-channel-bridge profile remove <name> --purge --yes
lark-channel-bridge profile export <name> [--output ./profile.json] [--force]
lark-channel-bridge profile export <name> --include-secrets --yes

profile remove archives local state by default, including the active profile. If other profiles remain, the bridge switches to the next one; if it was the last profile, the root config is cleared so the same name can be created again. --purge --yes permanently deletes local state. profile export redacts app secrets by default; --include-secrets --yes includes sensitive config.

If a profile was created with the wrong agent kind, stop or unregister any matching background service first, then run profile remove <name> and recreate it with the intended --agent.

Slash commands inside Feishu / Lark

Command Effect
/new, /reset Clear the current session
/new chat [name] Spawn a new group with the bot as owner and the requester as workbench owner (fork addition; see "What this fork adds → /new chat")
/cd <path> Switch working directory and reset the session
/ws list List named workspaces
/ws save <name> Save the current working directory as a named workspace
/ws use <name> Switch to a named workspace
/ws remove <name> Delete a named workspace
/resume Resume compatible history for the same agent, working directory, and permission mode
/status Show profile, agent, working directory, session, lark-cli identity, and run state
/config Adjust presentation preferences, access settings, and lark-cli identity policy
/invite user @name Allow a user to use the bot in DMs
/invite admin @name Add an access-control admin
/invite group Allow the current group to use the bot
/invite all group Allow all groups the bot has joined
/remove user @name, /remove admin @name, /remove group Remove access entries
/stop Stop the current run, including the card stop button
/timeout [N|off|default] Set or clear the current session idle watchdog
/ps List local bridge processes
/exit <id|#> Stop a bridge process
/reconnect Force a WebSocket reconnect
/doctor [description] Run low-sensitive diagnostics
/help Help card

DMs do not require an @ mention. Groups and topic groups require @bot by default; @all is ignored. Cloud-doc comments in supported document types run when the bot is mentioned. Workbench groups spawned by /new chat are an exception: the workbench owner does not need to @bot in those groups.

Reply Display and COT

/config controls the runtime model and three presentation settings:

  • Model: Claude and Codex use curated pickers. For opencode, enter a provider-qualified id such as openai/gpt-5; leave it empty to follow the opencode configuration.
  • Message reply mode: message card streams the final reply; plain text sends once after the run finishes.
  • Tool-call display: controls whether tool blocks appear in the final card / markdown reply.
  • COT process message: off sends only the final reply; brief first sends a COT message with agent progress text and tool summaries; detailed also includes tool args and truncated output.

When COT is enabled, the bridge splits the process view and final answer into two messages. The COT message is for tracing what the agent did; the final answer is still generated from the agent's raw text, without heuristic bridge-side filtering. If an agent emits final-answer text as ordinary stream text, that text can also appear in the COT process message.

lark-cli identity policy

Each profile uses a profile-local lark-cli directory at ~/.lark-channel/profiles/<profile>/lark-cli. The agent process receives LARKSUITE_CLI_CONFIG_DIR for that directory, so personal authorization in one profile is not shared with another profile.

The default policy is bot-only: lark-cli uses the app/bot identity and does not access personal resources. When a user authorizes personal resources such as calendar, mail, or drive, the current profile can switch to user-default, which keeps app identity available and also allows the authorized user identity. Owner/admin users can inspect or change this policy in /config; /status shows the current summary as lark-cli: app or lark-cli: user-ready.

Working directories

Each profile may define a default working directory through workspaces.default. New profiles may be created with --workspace <path>; if omitted, the bridge creates a profile-managed default working directory.

This is a profile-field snippet. Do not replace the whole config.json with it; edit the matching profile's workspaces field.

{
  "workspaces": {
    "default": "/Users/me/.lark-channel-workspaces/claude/default"
  }
}

The bridge checks that a selected directory exists, is a directory, and is not an overly broad location such as /, the home root, a system directory, or a temp root. The working directory is only the current directory for an agent run. It is not a filesystem sandbox; actual file access still depends on the local agent process and its permission mode.

Permission modes

The recommended user-facing profile config is permissions.defaultAccess and permissions.maxAccess. New profiles default to full for both values so the bridge can keep local tools, authorization flows, file writes, and other agent features fully usable. To tighten a profile, set one or both values to workspace or read-only; stricter modes can limit local tool execution, login/authorization flows, file writes, and similar capabilities.

This is a profile-field snippet. Do not replace the whole config.json with it; edit the matching profile's permissions field.

{
  "permissions": {
    "defaultAccess": "full",
    "maxAccess": "full"
  }
}

Mode mapping:

Bridge access Claude permission mode Codex mode
full bypassPermissions danger-full-access
workspace acceptEdits workspace-write
read-only plan read-only

The legacy sandbox field is still readable for old configs. After the bridge saves the profile, it migrates that setting to canonical permissions.

Data directories

Path Content
~/.lark-channel/config.json Root config with profiles and active profile
~/.lark-channel/active-profile Last selected profile
~/.lark-channel/profiles/<profile>/sessions.json Session state
~/.lark-channel/profiles/<profile>/sessions.json.catalog.json Agent-aware session catalog
~/.lark-channel/profiles/<profile>/workspaces.json Current and named workspace bindings
~/.lark-channel/profiles/<profile>/secrets.enc Profile-local encrypted secrets
~/.lark-channel/profiles/<profile>/lark-cli/ Profile-local lark-cli directory
~/.lark-channel/profiles/<profile>/media/ Attachment cache
~/.lark-channel/profiles/<profile>/logs/ Structured run logs
~/.lark-channel/registry/processes.json Local process registry
~/.lark-channel/registry/locks/ Profile and app locks

Set LARK_CHANNEL_HOME=/path/to/state to move all local bridge state. LARK_CHANNEL_LOG_DAYS overrides log retention.

Access control

Chat access is private by default: out of the box, only you can use the bot in DMs and groups. "You" = whoever created / owns the Feishu app (the person who scanned the QR to set it up). The bot figures out who the app owner is automatically from Feishu, so solo chat use needs zero configuration — you can DM it and @-mention it in any group, and everyone else's chat messages are silently ignored (no "permission denied" reply, which would only confirm the bot exists). Cloud-doc comments are document-scoped; see below.

To let other people or groups in, add them to one of three lists:

List Controls Add Remove
Allowed users who can DM the bot /invite user @them /remove user @them
Allowed chats which groups the bot answers in (for everyone in them) /invite group (current group) / /invite all group (every group the bot is in) /remove group (current group)
Admins who can change settings, and use the bot in any group /invite admin @them /remove admin @them

/invite and /remove can only be run by you (the creator) and admins. The @ in the command points at the target person (not the bot) — the bot resolves the mention to their identity, so you never deal with raw IDs.

Two identities that bypass everything

  • You (the creator): subject to no list at all — DMs, any group, every command. You can never lock yourself out: even if the lists get messed up, DM the bot and send /config to get back in. Transfer the app's ownership in the Feishu console and the bot follows the new owner automatically.
  • Admins: can DM, run management commands like /config, and bypass the allowed-chats list — the bot answers them in any group, listed or not. Good for teammates who co-maintain the bot.

Common setups

  • Just me → nothing to do; this is the default.
  • Let a teammate DM the bot/invite user @them
  • Open a work group to everyone in it → send /invite group inside that group
  • First-time setup, onboard every group the bot is already in/invite all group pulls them all into the list at once; trim with /remove group afterwards
  • Add a co-admin/invite admin @them

Worth knowing

  • Changes take effect on the next message — no restart needed.
  • In groups you must @ the bot first (DMs don't need it). That's a separate toggle (/config → "require @ in groups"), independent of the lists above.
  • Strangers get pure silence — no reply at all. The one exception: if someone @-mentions the bot in a group that hasn't been opened up, the bot posts a friendly one-liner telling them an admin can run /invite group to enable it.
  • Cloud-doc comments are document-scoped: anyone who can comment in a supported document and mention the bot can trigger a reply.

Advanced: editing the config file directly

If you'd rather not do it inside Feishu, /invite and /config write the matching profile's access field in ~/.lark-channel/config.json. Empty lists mean nobody from that list, not open access. This is a profile-field snippet; do not replace the whole config.json with it:

{
  "schemaVersion": 2,
  "profiles": {
    "claude": {
      "agentKind": "claude",
      "access": {
        "allowedUsers": ["ou_xxxxxxxxxxxxx"],
        "allowedChats": ["oc_xxxxxxxxxxxxx"],
        "admins": ["ou_xxxxxxxxxxxxx"],
        "requireMentionInGroup": true
      }
    }
  }
}

allowedUsers / admins take user open_ids; allowedChats takes group chat_ids. The easiest way to find an ID by hand: have the person message the bot (or @ it in the group), then check the active profile's log:

grep '"event":"enter"' ~/.lark-channel/profiles/<profile>/logs/bridge-$(date +%Y%m%d).jsonl | tail -5

Each line carries chatId (group / DM id) and senderId (user open_id). After a manual edit, restart the bridge or send /reconnect from an allowed admin context to apply it. For day-to-day tweaks /invite / /config are easier; direct edits are mainly for deployment scripts that pre-seed access.

Cloud-doc comments

Cloud-doc comments do not need a separate workspace binding or document allowlist. In supported document comments, mention the bot and the bridge replies in the same thread. Comment runs reuse the document session key and fall back to the user home directory when no document cwd was previously recorded.

FAQ

The bot stays silent or the local CLI never replies. Usually the local claude, codex, or opencode CLI is not logged in, or the current session points to a working directory that no longer exists. Send /status to inspect; /new often fixes it by starting a fresh session.

The agent subprocess looks frozen (card stuck on the last frame). The bridge supports an idle watchdog: if the agent emits nothing for N minutes, the process is killed and the card is annotated with the auto-termination reason. Disabled by default. Enable with /config globally, or /timeout 10 for the current session; /timeout off disables it for the session; /timeout default clears the session override.

The agent says it cannot see an image I sent. Upgrade to the latest version. Releases before 0.1.0 had a filename-dedup bug.

Testing and CI

Local checks:

pnpm test
pnpm typecheck
pnpm build

pnpm test includes unit, integration, and process-level adapter tests. CI runs on macOS, Ubuntu, and Windows with pnpm install --frozen-lockfile, pnpm test, pnpm typecheck, and pnpm build.

Optional telemetry

By default the bridge reports nothing: no metrics, no logs leave your machine, and it pulls in zero telemetry dependencies. The hook below is inert unless you opt in.

To wire up your own monitoring, point an environment variable at a module that default-exports (or exports createAdapter) an AdapterFactory:

LARK_CHANNEL_TELEMETRY_MODULE=your-telemetry-package lark-channel-bridge start

That module receives every log.* event plus error/metric hooks and forwards them wherever you like. The interface is exported from the package root:

import type { AdapterFactory, TelemetryAdapter, TelemetryEvent } from '@fullstackjam/lark-coding-agent-bridge';

const createAdapter: AdapterFactory = (meta) => ({
  emit(event) {/* ship event */},
  recordError(err, ctx) {/* ship exception */},
  recordMetric(name, value, tags) {/* ship metric */},
  flush(timeoutMs) {/* drain buffered events */},
});
export default createAdapter;

A missing module, a bad factory, or a throwing adapter all degrade to noop — telemetry can never stop the bridge from starting or break logging.

License

MIT

About

Bot that bridges Feishu/Lark messenger with a local Claude Code or Codex CLI. Streaming cards, per-chat sessions, multiple workspaces

Resources

License

Stars

2 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages

  • TypeScript 100.0%