botmesh command ships with @botmesh/core (with pnpm, run it as
pnpm exec botmesh dev src/bot.js). Projects generated with create-botmesh
already have it as npm run dev.
1. The factory pattern (defineBot)
Your module default-exports a factory that receives the provider and
returns the Bot without starting it. Whoever runs the file decides which
provider to inject:
- The factory can be async (
async (provider) => { ... }). isMainModule(import.meta)usesimport.meta.main(Node ≥ 22.18 / 24.2) or comparesprocess.argv[1](case-insensitive on Windows). It isfalsewhen a process manager imports your file from its own wrapper (pm2 in ESM mode); in that case use a separate start file that imports the factory.botmesh devsets the environment variableBOTMESH_DEV=1in case your code needs to detect the simulator.
2. Usage
Exit codes:
0 ok, 1 runtime failure, 2 usage error.
3. Commands inside the simulator
- Any line is a message to the bot. Slash commands (
/reset) are sent as messages. ::textsends text that starts with:. An emoticon like:)is a normal message.:as <user> [name]switches the sender (stays in the same chat).:group <id>moves into a group chat; mention the bot as@bot.:dmgoes back to the private chat.:media <image|video|audio|voice|document|sticker> <path> [caption]sends a file from disk (aliases:photo,doc,file,ptt; quote paths with spaces). The bytes arrive throughprovider.downloadMedia(ref).:click <n|id|label>taps a button, list row or card button, or votes on the last poll (it reachesbot.on('poll')).:stateshows who you are, the bot status and the live conversation (whether it is waiting for an answer, and itsc.state).:resetrestarts the bot from scratch (drops conversations and in-memory state).:reloadre-imports your code.:help,:quit(also Ctrl+C or Ctrl+D).
[1] Info); media prints
as [image: url] with the caption on the next line. Messages the bot sends
to other chats print as bot → <chatId> › ....
When input is piped (printf 'hi\n' | npx botmesh dev src/bot.js --no-watch),
each line waits until the bot has finished reacting (up to 30 s, to cover AI
calls), so transcripts stay in order.
4. Hot reload
- Saving a
.js/.mjs/.cjs/.ts/.mts/.cts/.jsx/.tsxfile in the watched folder stops the current bot (bot.stop()), re-imports the entry and builds a new bot. Relative imports (./flows/menu.js) reload too; packages (@botmesh/core,node_modules) do not. node_modules, dot-folders,sessions/,*_sessions,.json,.envand logs are ignored, so writing a store or credentials never loops.- If a reload fails (e.g. a syntax error), the previous version keeps running. In-memory conversations are lost on reload.
- Node cannot unload old modules, so memory grows in very long sessions:
restart now and then. Close resources you create outside the bot
(servers, intervals) in
bot.on('stopped', ...).
5. TypeScript
- It needs Node’s native type stripping: on by default in Node ≥ 22.18 /
23.6. On Node 22.6–22.17 run
NODE_OPTIONS=--experimental-strip-types npx botmesh dev src/bot.ts. - Relative imports must use the real
.tsextension and type-only imports must useimport type. No TSX, enums or namespaces (unless you enable transform-types). - When support is missing, the CLI prints these options instead of a cryptic error.
6. Safety
- If the factory builds the
Botwith a provider other than the one it receives, the simulator refuses to start it (it could connect for real), stops that bot and halts: it never calls the factory again, and in watch mode the CLI exits with code 1. - A default export not created with
defineBot()gets a one-time warning. - If the entry runs but does not export a factory (an old
bot.jsthat starts itself on import),botmesh devexits, even in watch mode, to close any real connection it opened.
7. Programmatic API (@botmesh/core/dev)
DevSessiondoes no I/O itself; injectloadandwrite. Options includeuser,name,group,echo,settleMs/maxSettleMsandonHalt.DevProviderextendsMockProviderwithonOutbound,injectMediaandinjectPollVote.- Pure helpers are exported too:
renderOutbound,parseDevInput,watchForChanges, andrunCli(argv, io)with injectable streams.
bot.inspectConversation({ from, sender, isGroup })
returns { key, state, waiting, queued, accepts? } or undefined (state is a
shallow copy; accepts says what the pending wait takes: 'text', a list of
message types for waitMedia, or 'any' for choose/waitAny), and bot.isBusy() tells whether the bot is still handling
an event or running a conversation step.