1data: {"type":"install_url","url":"https://kimaki.dev/discord-install?clientId=...&clientSecret=..."}
install_url ──▶ authorized ──▶ ready ▲ │ (user opens the URL and authorizes)
| Event | Payload | Description |
install_url | { type, url } | Discord install URL to send to the user |
authorized | { type, guild_id } | User authorized the bot in a server |
ready | { type, app_id, guild_ids } | Bot is connected and first-run onboarding is done |
error | { type, message, install_url? } | Something went wrong. install_url is set when the user must open the URL again |
install_url and
authorized and only emits ready.--gateway. If the bot is in several servers, pass --guild <id>, otherwise
startup fails with an error event.data: and followed by an empty line:1data: {"type":"ready","app_id":"1477605701202481173","guild_ids":["123..."]}\n\n
data:
prefix still lets a standard SSE parser ignore any other line.1npm install eventsource-parser
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849import { spawn } from 'node:child_process' import { createParser } from 'eventsource-parser' const child = spawn('kimaki', [ '--gateway', '--data-dir', '/data/user-abc', '--gateway-callback-url', 'https://your-platform.com/oauth-done', ], { env: { ...process.env, // Unique port per instance; a busy port makes startup fail KIMAKI_LOCK_PORT: '31200', }, stdio: ['ignore', 'pipe', 'pipe'], }) const parser = createParser({ onEvent(sseEvent) { const event = JSON.parse(sseEvent.data) switch (event.type) { case 'install_url': { // Send this URL to your user (email, web UI, etc.) console.log('Install URL:', event.url) break } case 'authorized': { // User authorized the bot; you now have their guild_id console.log('Guild ID:', event.guild_id) break } case 'ready': { // Bot is connected and listening for Discord messages console.log('App ID:', event.app_id) console.log('Guild IDs:', event.guild_ids) break } case 'error': { console.error('Error:', event.message) break } } }, }) // Feed raw stdout into the parser; it extracts data: lines child.stdout.on('data', (chunk) => { parser.feed(chunk.toString()) })
--install-url prints the install URL as plain text and exits. With
--gateway, it creates and saves gateway credentials first when the data
directory has none.1kimaki --gateway --install-url --data-dir /data/user-abc
kimaki --gateway with the same data directory, it reuses
these credentials and waits for the install to finish.kimaki bot install-url
prints the same URL.| Flag | Required | Description |
--gateway | yes | Use the shared Kimaki gateway bot |
--data-dir <path> | recommended | Isolated data directory per user instance |
--gateway-callback-url <url> | optional | Redirect the user here after the install instead of the default kimaki page |
--install-url | optional | Print the install URL and exit |
--guild <id> | when the bot is in several servers | Server to onboard |
--machine-name <name> | optional | Name of the Discord category, Kimaki <name> (default: hostname) |
--restart-onboarding | optional | Run the install flow again, even with saved credentials |
--no-analytics | optional | Disable anonymous usage analytics |
curl -fsSL https://opencode.ai/v2/install | bash before it emits any event.
Install it in your image ahead of time to make startup faster.--gateway-callback-url to redirect the user to your own page after they
authorize the bot. The URL must use https, or http on localhost or
127.0.0.1.1kimaki --gateway --gateway-callback-url https://your-platform.com/setup-done
install_url event includes the callback:1https://kimaki.dev/discord-install?clientId=...&clientSecret=...&kimakiCallbackUrl=https%3A%2F%2Fyour-platform.com%2Fsetup-done
guild_id and client_id query
parameters appended. Your platform knows which server was authorized for which
instance.KIMAKI_LOCK_PORT (default 29988). If the
port is busy, the new process fails to start with port is in use. It never
stops the process that already runs.123456const lockPort = 31100 + userIndex const child = spawn('kimaki', ['--gateway', '--data-dir', userDataDir], { env: { ...process.env, KIMAKI_LOCK_PORT: String(lockPort) }, stdio: ['ignore', 'pipe', 'pipe'], })
--data-dir per user so their SQLite databases, logs, and
credentials are isolated. CLI subcommands for an instance need the same
KIMAKI_LOCK_PORT and --data-dir.