install_url ──▶ authorized ──▶ ready ▲ │ (user clicks URL and authorizes)
| Event | Payload | Description |
install_url | { type, url } | Discord OAuth URL to send to the user |
authorized | { type, guild_id } | User authorized the bot in a guild |
ready | { type, app_id, guild_ids } | Bot is connected and listening |
error | { type, message, install_url? } | Something went wrong |
data: and terminated with \n\n:1data: {"type":"install_url","url":"https://kimaki.dev/discord-install?clientId=...&callbackUrl=..."}\n\n
data: prefix is what makes it robust — log
lines, warnings, spinner output, and other noise do not start with data: at
column 0, so the parser ignores them completely.1npm install eventsource-parser
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950import { spawn } from 'node:child_process' import { createParser } from 'eventsource-parser' const child = spawn('kimaki', [ '--gateway', '--restart-onboarding', '--data-dir', '/data/user-abc', '--gateway-callback-url', 'https://your-platform.com/oauth-done', ], { env: { ...process.env, // Unique port per instance to avoid conflicts between concurrent kimaki processes 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 fully 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, ignores everything else child.stdout.on('data', (chunk) => { parser.feed(chunk.toString()) })
| Flag | Required | Description |
--gateway | yes | Use the shared Kimaki gateway bot |
--restart-onboarding | for fresh setup | Force the onboarding flow even if saved credentials exist |
--data-dir <path> | recommended | Isolated data directory per user instance |
--gateway-callback-url <url> | optional | Redirect user here after OAuth instead of default kimaki page |
--gateway-callback-url to redirect the user to your own page after they
authorize the bot. The callback URL receives a ?guild_id=<id> query parameter
so your platform knows which guild was authorized.1kimaki --gateway --gateway-callback-url https://your-platform.com/setup-done
install_url event will include the callback:1https://kimaki.dev/discord-install?clientId=...&callbackUrl=https%3A%2F%2Fyour-platform.com%2Fsetup-done
?guild_id=<id> appended.KIMAKI_LOCK_PORT to avoid conflicts.
Without it, a new process will kill the existing one.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.{ is fragile — a log line could start with { by coincidence.data: at column 0 are parsed as eventsid:, event:, retry:) are ignored by the parser if
you only read .datadata events correctly