Conversation
## Problem
MiniMax Code is a terminal application. On the machine it is fine; away from it,
the only options are a remote desktop session or a tunnel, and neither works well
from a phone. This Plugin turns a Feishu conversation into a remote control for a
local mcode install.
A message sent in Feishu becomes one `mcode exec` turn in a per-conversation
workspace. The answer is written back by editing that same message, so a turn that
makes twelve tool calls still produces one chat bubble instead of twelve.
## Example prompt
create a file notes.md in the current directory with the content
"shopping list", then read it back to confirm
## Expected result
The bot's single reply, edited in place:
Created and read back successfully.
`...workspaces/oc_xxxxxx/notes.md` - 16 bytes, content:
```
shopping list
```
---
11.4s - 2 tool calls
## Package shape
Skill-only. No `mcp.json`, no `package.json`, no `node_modules`, no native
binaries, no symlinks, no install step.
The bridge drives two CLIs the user already has, discovered from `PATH` and from
the install layout. No drive letter or absolute prefix is hardcoded anywhere, and
`$PLUGIN_DATA` is honoured when the runtime provides it.
- `plugin.json` - portable Agent Plugins 1.0 manifest, validated by
`scripts/validate.mjs`
- `.claude-plugin/plugin.json` - v0.4.0+ manifest, `skills` pointing at both copies
- `skills/SKILL.md` and `skills/mcode-feishu-bridge/SKILL.md` - byte-identical,
per the recommended cross-version layout
- `scripts/mcode-feishu-bridge.mjs` - the bridge
- `scripts/mcode-feishu-bridge.test.mjs` - the suite
## Design decisions
**`shell: false` everywhere.** mcode answers can contain resource markup such as
`<media type="file" src="..." />`. The `<`, `>` and `"` in that are redirection
and quote operators to `cmd`, so forwarding arguments through `cmd /c` shreds the
command: exit 1, empty stdout, empty stderr, no diagnosable cause. Measured with
one payload against one message, `cmd /c` failed and a direct spawn delivered.
Every child process here is spawned directly and the shell is never involved.
**One edit queue per message.** Tool-broadcast hints and the final answer used to
be two concurrent `lark-cli` processes, and Feishu is last-write-wins, so a slow
hint could overwrite the answer and leave the chat stuck on a tool call. A
reproduction lost 1 round in 6. All edits now go through a serial queue that is
sealed before the final is enqueued.
**The watermark always advances.** Stopping it on a delivery failure was meant to
enable a retry, but it made the whole poll restart: every cycle re-ran mcode and
re-sent a placeholder, so a persistent failure became a chat flood. Failures now
queue a delivery-only retry with linear backoff and a hard cap.
**Fetch problems throw.** Returning a status code and trusting a caller to check
it is not a safeguard: deleting that one line left the suite green. `collectFresh`
throws, so the only exit is a catch that already logs.
**Newest page first, full pagination only when needed.** `--order asc
--page-size 50` returns the oldest 50 messages. Once a conversation outgrows one
page, new messages are invisible and the bridge looks alive but deaf while logging
nothing. It now fetches the newest page and escalates to `--page-all` only when the
watermark is not on it.
**State is written atomically.** Staged, fsynced, renamed. An interrupted write
would otherwise leave a truncated `state.json`, and `loadState` falls back to an
empty state, which would reprocess the whole conversation.
**`--permission full`, stated plainly.** A phone is a bad place to answer approval
prompts, so the prompts are removed. That means a Feishu message can cause
arbitrary local changes, and the README says so under "Security model" rather than
burying it.
## Data and network
The Plugin contains no credentials and never signs in. It shells out to the user's
own `lark-cli`. The only destinations are the Feishu/Lark Open Platform, for
message read, send, edit, and attachment download. No telemetry, no analytics, no
update check, no other host. `README.md` carries four separate disclosure
sections: no credentials of its own, dependency on a `lark-cli` configuration the
user already owns, no telemetry, and Lark/Feishu as the third-party service.
There is no default chat id, because a chat id is a private identifier. It comes
from `--chat`, `$MCODE_FEISHU_CHAT`, or `config.json`.
## Validation
node scripts/validate.mjs
-> Validated 29 hosted Plugins and all examples.
node scripts/mcode-feishu-bridge.test.mjs
-> 84 passed, 0 failed, 0 skipped
node --test # whole repository
-> tests 346, pass 344, fail 1, skipped 1
the single failure is pre-existing and unrelated:
tests/plugins/octopus-meme-maker/smoke.test.mjs asserts that
scripts/make_gif.py parses, and this host has no Python
The suite imports the real module under `MCODE_FEISHU_BRIDGE_TEST=1` rather than
a copy, so a green run is evidence about the shipped code.
### Test evidence
Two cases are regression tests for defects that shipped in an earlier draft and
were found in real use. Their comments record the measured numbers.
- `U` reproduces the paging defect from a real conversation that had outgrown one
page: 50 oldest messages, the watermark on the last of them, and the new message
on the next page. It asserts the old logic drops the message and the new logic
recovers it.
- `W` asserts that a fetch problem throws rather than returning an empty list.
Every other case guards a contract that was found by negative injection rather than
by inspection: each contract was first broken on purpose, the suite was confirmed
to go red, and only then was the behaviour treated as covered. Nine such
injections were run against the delivery and locking logic, and eight against the
paging fix; all were caught.
Cases `O` and `V` read this Plugin's own source and fail if a child process is ever
routed through a shell, or if a message fetch is ever issued as
`--order asc` without `--page-all`.
### CI behaviour
Case `Q` spawns a real mcode process to prove the hard timeout kills a process
tree. It skips when `resolveMcodeCli()` returns nothing, and the import-time
executable check does not exit under `MCODE_FEISHU_BRIDGE_TEST=1`, so the file does
not fail on a host without mcode.
Verified on a host with no mcode installed (`USERPROFILE` and `MCODE_HOME` pointed
at empty directories):
78 passed, 0 failed, 1 skipped
## Design compliance
- One Plugin per folder, one contribution per pull request. No other contributor's
Plugin is touched.
- Zero npm dependencies; no `package.json`, no lockfile, no install step.
- `plugin.json` declares `$schema`, `name`, `version`, `description`, `author`,
`homepage`, `repository`, `license`, `keywords`, and no unknown field.
- `README.md` and Apache-2.0 `LICENSE` are present; the manifest license matches.
- Both runtime layouts are shipped in parallel, with byte-identical Skill copies.
- No credentials, private endpoints, hidden telemetry, installers, native
binaries, or symlinks.
- No host-literal paths. Verified by scanning for drive-letter, `/Users/`, and
`/home/` literals; the only regex match in the source is a false positive inside
a template literal.
- No `TODO`, no UTF-8 BOM, LF endings enforced by a Plugin-local `.gitattributes`.
- `state.json` writes are atomic.
## Known limits
Stated in the README rather than left to be discovered:
- one turn at a time per conversation; a second message waits
- no card messages; plain text messages are edited in place
- the bridge never recalls anything
- long replies are not yet tested against the Feishu per-message length limit
- only `text`, `post`, `image` and `file` message types are parsed
…d the failure modes The Setup section said "a lark-cli you have already configured" and stopped there. That is not enough, because the bridge reads and writes through different identities and a half-configured app fails in a way the log does not make obvious. The bridge calls `--as user` for +chat-messages-list and +messages-resources-download, and `--as bot` for +messages-send, +messages-reply and +messages-edit. A bot cannot see a p2p conversation history, and only a bot may edit a message it sent, so both identities are genuinely required and they fail independently. Documented: - `lark-cli auth status` must show `identities.bot.status` and `identities.user.status` both `ready`. - The minimum scope set, split by identity: `im:message:readonly` and `im:resource` on the user side; `im:message` and `im:message:update` on the bot side. Called out `offline_access` separately, because without it the user token stops working after a couple of hours and the bridge goes quiet at a moment when nothing reports an error. - That the user identity is the user"s own account, so the bridge acts as them. - A p2p chat with the app as the simplest arrangement, which is what this Plugin is built and tested for. - First-run behaviour: the workspace is created on start and nothing historical is picked up, so the first message has to come after the bridge starts. - Obtaining a chat id, including the `--page-all` that the listing needs. Also added, because an operator hits them immediately and none were documented: - Running it in the background, with the reason the Plugin ships no autostart and the specific trap that a supervisor inheriting the child"s std handles blocks forever. This is the defect that cost the most time during development. - Uninstall: `--stop`, then delete the data directory. Removing the Plugin leaves that directory alone on purpose; it is user data. - Network: outbound HTTPS to the Feishu/Lark Open Platform only, no inbound listener, and `lark-cli` owns proxy configuration. - A symptom-to-cause table for the identity, scope, token-expiry, attachment, paging and lock failures, noting that every one of them writes a log line, so an empty log plus a silent chat means the watcher is not running. skills/SKILL.md carries the same identity and scope requirements in its pre-flight and troubleshooting sections, so the agent checks them before starting rather than after a user reports silence. Validation: `node scripts/validate.mjs` green, 84 passed / 0 failed.
…reference to the README Audited the shipped docs against the three questions a reader actually arrives with: what does it do, how do I set it up, how do I use it. Setup was complete after the previous commit. The other two were not, and both gaps were of the same kind: the information existed, but somewhere other than where a reader would look for it. - There was no positive feature list. The README had one worked example and a "what it deliberately does not do" section, so a reader had to infer the capabilities. Added "What you get": one message per task, live tool progress, real tool access, continuous and isolated per-conversation context, attachments, a hung turn being killed rather than waited on, the footer, delivery that does not lose the result, single-instance enforcement, and a log readable while running. - There was no package tree. Added "What you in the package", including the note that the two Skill copies are byte-identical because the two runtimes discover Skills differently, and that there is no mcp.json, package.json or binary. - There was no command or flag reference. The flags table existed only in SKILL.md, so answering "how do I run it" from the README alone was not possible. Added a Usage section with the three commands, the full flag table, and the two ways to configure the conversation once instead of on every start. Also cross-linked the failure table from README to the SKILL troubleshooting order, so an agent and a human are looking at the same list. Validation: validator green, 84 passed / 0 failed, both Skill copies still byte-identical.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this Plugin does
MiniMax Code is a terminal application. On the machine it is fine; away from it, the only options are a remote desktop session or a tunnel, and neither works well from a phone. This Plugin turns a Feishu conversation into a remote control for a local mcode install.
A message sent in Feishu becomes one
mcode execturn in a per-conversation workspace. The answer is written back by editing that same message, so a turn that makes twelve tool calls still produces one chat bubble instead of twelve.Example prompt
Expected result
The bot's single reply, edited in place:
Package shape
Skill-only. No
mcp.json, nopackage.json, nonode_modules, no native binaries, no symlinks, no install step.The bridge drives two CLIs the user already has, discovered from
PATHand from the install layout. No drive letter or absolute prefix is hardcoded anywhere, and$PLUGIN_DATAis honoured when the runtime provides it.plugin.jsonscripts/validate.mjs.claude-plugin/plugin.jsonskillspointing at both copiesskills/SKILL.mdskills/mcode-feishu-bridge/SKILL.mdscripts/mcode-feishu-bridge.mjsscripts/mcode-feishu-bridge.test.mjsDependencies and platforms
lark-cli(@larksuite/cli), already configured for the user's account. ThePlugin never signs in; if
lark-cliis not authenticated it reports the failureand stops.
Windows and macOS/Linux. Windows-specific paths (a process-tree kill,
where) areselected at runtime behind
IS_WIN; everything else is portable.Data flow and network
The Plugin contains no credentials of its own and never signs in. It shells out to
the user's own
lark-cli; the user's credentials live in the user's ownlark-cliconfig directory.Destinations, and only these:
open.feishu.cn/open.larksuite.comlark-clilark-clisends; this Plugin neither reads nor stores itNo telemetry, no analytics, no crash reporting, no update check, no registry, no
CDN, no other host.
Files written under the data directory (
$PLUGIN_DATA, else$MCODE_FEISHU_BRIDGE_DATA, else~/.mcode-feishu-bridge):state.json(per chat:workspace, mcode session id, last handled message id, turn count, at most one
pending delivery),
config.json,bridge.log,bridge.lock, the per-chatworkspaces/directory, and downloadedmedia/.state.jsonis writtenatomically, so an interrupted write cannot leave a truncated file that would make
the bridge reprocess a whole conversation.
There is no default chat id, because a chat id is a private identifier. It comes
from
--chat,$MCODE_FEISHU_CHAT, orconfig.json.README.mdcarries four separate disclosure sections: no credentials of its own,dependency on a
lark-cliconfiguration the user already owns, no telemetry, andLark/Feishu as the third-party service.
Design decisions
shell: falseeverywhere. mcode answers can contain resource markup such as<media type="file" src="..." />. The<,>and"in that are redirection andquote operators to
cmd, so forwarding arguments throughcmd /cshreds thecommand: exit 1, empty stdout, empty stderr, no diagnosable cause. Measured with one
payload against one message,
cmd /cfailed and a direct spawn delivered. Everychild process here is spawned directly and the shell is never involved.
One edit queue per message. Tool-broadcast hints and the final answer used to be
two concurrent
lark-cliprocesses, and Feishu is last-write-wins, so a slow hintcould overwrite the answer and leave the chat stuck on a tool call. A reproduction
lost 1 round in 6. All edits now go through a serial queue sealed before the final
is enqueued.
The watermark always advances. Stopping it on a delivery failure was meant to
enable a retry, but it made the whole poll restart: every cycle re-ran mcode and
re-sent a placeholder, so a persistent failure became a chat flood. Failures now
queue a delivery-only retry with linear backoff and a hard cap.
Fetch problems throw. Returning a status code and trusting a caller to check it
is not a safeguard: deleting that one line left the suite green.
collectFreshthrows, so the only exit is a catch that already logs.
Newest page first, full pagination only when needed.
--order asc --page-size 50returns the oldest 50 messages. Once a conversation outgrows one page, newmessages are invisible and the bridge looks alive but deaf while logging nothing. It
now fetches the newest page and escalates to
--page-allonly when the watermark isnot on it.
State is written atomically. Staged, fsynced, renamed.
--permission full, stated plainly. A phone is a bad place to answer approvalprompts, so the prompts are removed. That means a Feishu message can cause arbitrary
local changes, and the README says so under "Security model" rather than burying it.
Validation
The suite imports the real module under
MCODE_FEISHU_BRIDGE_TEST=1rather than acopy, so a green run is evidence about the shipped code.
Test evidence
Two cases are regression tests for defects that shipped in an earlier draft and were
found in real use:
Ureproduces the paging defect from a real conversation that had outgrown onepage: 50 oldest messages, the watermark on the last of them, the new message on
the next page. It asserts the old logic drops the message and the new logic
recovers it.
Wasserts that a fetch problem throws rather than returning an empty list.Every other case guards a contract found by negative injection rather than by
inspection: the contract was broken on purpose, the suite was confirmed to go red,
and only then was the behaviour treated as covered. Nine such injections were run
against the delivery and locking logic and eight against the paging fix; all were
caught. Two earlier attempts were
MISSEDand produced the two extra cases.Cases
OandVread this Plugin's own source and fail if a child process is everrouted through a shell, or if a message fetch is ever issued as
--order ascwithout
--page-all.CI behaviour
Case
Qspawns a real mcode process to prove the hard timeout kills a process tree.It skips when
resolveMcodeCli()returns nothing, and the import-time executablecheck does not exit under
MCODE_FEISHU_BRIDGE_TEST=1, so the file does not fail ona host without mcode. Verified with
USERPROFILEandMCODE_HOMEpointed at emptydirectories:
Design compliance
Plugin is touched.
package.json, no lockfile, no install step.plugin.jsondeclares only$schema,name,version,description,author,homepage,repository,license,keywords, with no unknown field.README.mdand an Apache-2.0LICENSEare present; the manifest license matches.or symlinks.
/Users/and/home/literals; the only regex match in the source is a false positive inside atemplate literal.
TODO, no UTF-8 BOM, LF endings enforced by a Plugin-local.gitattributes.Known limits
Stated in the README rather than left to be discovered:
text,post,imageandfilemessage types are parsedconversation
Manual test evidence
Driven for two hours against a real Feishu conversation on Windows 11, mcode 0.5.10:
⚙️ Calling \write`…` then the answera<b>c & d>e | fsurvived end to end, 0 replacement characters<media type="file" src="..." />delivered intactThe three defects the comments record (shell double-parsing, edit ordering, and
paging) were each found in that real use, reproduced in isolation, and are now
covered by the suite.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.