@otto-assistant/bridge 0.4.97 → 0.4.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,342 @@
1
+ import crypto from 'node:crypto'
2
+ import { spawn, type ChildProcess } from 'node:child_process'
3
+ import net from 'node:net'
4
+ import {
5
+ ChannelType,
6
+ MessageFlags,
7
+ type TextChannel,
8
+ type ThreadChannel,
9
+ } from 'discord.js'
10
+ import { TunnelClient } from 'traforo/client'
11
+ import type { CommandContext } from './types.js'
12
+ import {
13
+ resolveWorkingDirectory,
14
+ SILENT_MESSAGE_FLAGS,
15
+ } from '../discord-utils.js'
16
+ import { createLogger } from '../logger.js'
17
+
18
+ const logger = createLogger('VSCODE')
19
+ const SECURE_REPLY_FLAGS = MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS
20
+ const MAX_SESSION_MINUTES = 30
21
+ const MAX_SESSION_MS = MAX_SESSION_MINUTES * 60 * 1000
22
+ const TUNNEL_BASE_DOMAIN = 'kimaki.dev'
23
+ const TUNNEL_ID_BYTES = 16
24
+ const READY_TIMEOUT_MS = 60_000
25
+ const LOCAL_HOST = '127.0.0.1'
26
+
27
+ export type VscodeSession = {
28
+ coderaftProcess: ChildProcess
29
+ tunnelClient: TunnelClient
30
+ url: string
31
+ workingDirectory: string
32
+ startedBy: string
33
+ startedAt: number
34
+ timeoutTimer: ReturnType<typeof setTimeout>
35
+ }
36
+
37
+ const activeSessions = new Map<string, VscodeSession>()
38
+
39
+ export function createVscodeTunnelId(): string {
40
+ return crypto.randomBytes(TUNNEL_ID_BYTES).toString('hex')
41
+ }
42
+
43
+ export function buildCoderaftArgs({
44
+ port,
45
+ workingDirectory,
46
+ }: {
47
+ port: number
48
+ workingDirectory: string
49
+ }): string[] {
50
+ return [
51
+ 'coderaft',
52
+ '--port',
53
+ String(port),
54
+ '--host',
55
+ LOCAL_HOST,
56
+ '--without-connection-token',
57
+ '--disable-workspace-trust',
58
+ '--default-folder',
59
+ workingDirectory,
60
+ ]
61
+ }
62
+
63
+ function createPortWaiter({
64
+ port,
65
+ process: proc,
66
+ timeoutMs,
67
+ }: {
68
+ port: number
69
+ process: ChildProcess
70
+ timeoutMs: number
71
+ }): Promise<void> {
72
+ return new Promise((resolve, reject) => {
73
+ const maxAttempts = Math.ceil(timeoutMs / 100)
74
+ let attempts = 0
75
+
76
+ const check = (): void => {
77
+ if (proc.exitCode !== null) {
78
+ reject(new Error(`coderaft exited with code ${proc.exitCode} before becoming ready`))
79
+ return
80
+ }
81
+
82
+ const socket = net.createConnection(port, LOCAL_HOST)
83
+ socket.on('connect', () => {
84
+ socket.destroy()
85
+ resolve()
86
+ })
87
+ socket.on('error', () => {
88
+ socket.destroy()
89
+ attempts += 1
90
+ if (attempts >= maxAttempts) {
91
+ reject(new Error(`Port ${port} not reachable after ${timeoutMs}ms`))
92
+ return
93
+ }
94
+ setTimeout(check, 100)
95
+ })
96
+ }
97
+
98
+ check()
99
+ })
100
+ }
101
+
102
+ function getAvailablePort(): Promise<number> {
103
+ return new Promise((resolve, reject) => {
104
+ const server = net.createServer()
105
+ server.on('error', reject)
106
+ server.listen(0, LOCAL_HOST, () => {
107
+ const address = server.address()
108
+ if (!address || typeof address === 'string') {
109
+ server.close(() => {
110
+ reject(new Error('Failed to resolve an available port'))
111
+ })
112
+ return
113
+ }
114
+ const port = address.port
115
+ server.close((error) => {
116
+ if (error) {
117
+ reject(error)
118
+ return
119
+ }
120
+ resolve(port)
121
+ })
122
+ })
123
+ })
124
+ }
125
+
126
+ function cleanupSession(session: VscodeSession): void {
127
+ clearTimeout(session.timeoutTimer)
128
+ try {
129
+ session.tunnelClient.close()
130
+ } catch {}
131
+ if (session.coderaftProcess.exitCode === null) {
132
+ try {
133
+ session.coderaftProcess.kill('SIGTERM')
134
+ } catch {}
135
+ }
136
+ }
137
+
138
+ export function getActiveVscodeSession({ sessionKey }: { sessionKey: string }): VscodeSession | undefined {
139
+ return activeSessions.get(sessionKey)
140
+ }
141
+
142
+ export function stopVscode({ sessionKey }: { sessionKey: string }): boolean {
143
+ const session = activeSessions.get(sessionKey)
144
+ if (!session) {
145
+ return false
146
+ }
147
+
148
+ activeSessions.delete(sessionKey)
149
+ cleanupSession(session)
150
+ logger.log(`VS Code stopped (key: ${sessionKey})`)
151
+ return true
152
+ }
153
+
154
+ export async function startVscode({
155
+ sessionKey,
156
+ startedBy,
157
+ workingDirectory,
158
+ }: {
159
+ sessionKey: string
160
+ startedBy: string
161
+ workingDirectory: string
162
+ }): Promise<VscodeSession> {
163
+ const existing = activeSessions.get(sessionKey)
164
+ if (existing) {
165
+ return existing
166
+ }
167
+
168
+ const port = await getAvailablePort()
169
+ const tunnelId = createVscodeTunnelId()
170
+ const args = buildCoderaftArgs({
171
+ port,
172
+ workingDirectory,
173
+ })
174
+ const coderaftProcess = spawn('bunx', args, {
175
+ cwd: workingDirectory,
176
+ stdio: ['ignore', 'pipe', 'pipe'],
177
+ env: {
178
+ ...process.env,
179
+ PORT: String(port),
180
+ },
181
+ })
182
+
183
+ coderaftProcess.stdout?.on('data', (data: Buffer) => {
184
+ logger.log(data.toString().trim())
185
+ })
186
+ coderaftProcess.stderr?.on('data', (data: Buffer) => {
187
+ logger.error(data.toString().trim())
188
+ })
189
+
190
+ try {
191
+ await createPortWaiter({
192
+ port,
193
+ process: coderaftProcess,
194
+ timeoutMs: READY_TIMEOUT_MS,
195
+ })
196
+ } catch (error) {
197
+ if (coderaftProcess.exitCode === null) {
198
+ coderaftProcess.kill('SIGTERM')
199
+ }
200
+ throw error
201
+ }
202
+
203
+ const tunnelClient = new TunnelClient({
204
+ localPort: port,
205
+ localHost: LOCAL_HOST,
206
+ tunnelId,
207
+ baseDomain: TUNNEL_BASE_DOMAIN,
208
+ })
209
+
210
+ try {
211
+ await Promise.race([
212
+ tunnelClient.connect(),
213
+ new Promise<never>((_, reject) => {
214
+ setTimeout(() => {
215
+ reject(new Error('Tunnel connection timed out after 15s'))
216
+ }, 15_000)
217
+ }),
218
+ ])
219
+ } catch (error) {
220
+ tunnelClient.close()
221
+ if (coderaftProcess.exitCode === null) {
222
+ coderaftProcess.kill('SIGTERM')
223
+ }
224
+ throw error
225
+ }
226
+
227
+ const url = tunnelClient.url
228
+
229
+ const timeoutTimer = setTimeout(() => {
230
+ logger.log(`VS Code auto-stopped after ${MAX_SESSION_MINUTES} minutes (key: ${sessionKey})`)
231
+ stopVscode({ sessionKey })
232
+ }, MAX_SESSION_MS)
233
+ timeoutTimer.unref()
234
+
235
+ const session: VscodeSession = {
236
+ coderaftProcess,
237
+ tunnelClient,
238
+ url,
239
+ workingDirectory,
240
+ startedBy,
241
+ startedAt: Date.now(),
242
+ timeoutTimer,
243
+ }
244
+
245
+ coderaftProcess.once('exit', (code, signal) => {
246
+ const current = activeSessions.get(sessionKey)
247
+ if (current !== session) {
248
+ return
249
+ }
250
+ logger.log(`VS Code process exited (key: ${sessionKey}, code: ${code}, signal: ${signal ?? 'none'})`)
251
+ stopVscode({ sessionKey })
252
+ })
253
+
254
+ activeSessions.set(sessionKey, session)
255
+ logger.log(`VS Code started by ${startedBy}: ${url}`)
256
+ return session
257
+ }
258
+
259
+ export async function handleVscodeCommand({
260
+ command,
261
+ }: CommandContext): Promise<void> {
262
+ const channel = command.channel
263
+ if (!channel) {
264
+ await command.reply({
265
+ content: 'This command can only be used in a channel.',
266
+ flags: SECURE_REPLY_FLAGS,
267
+ })
268
+ return
269
+ }
270
+
271
+ const isThread = [
272
+ ChannelType.PublicThread,
273
+ ChannelType.PrivateThread,
274
+ ChannelType.AnnouncementThread,
275
+ ].includes(channel.type)
276
+ const isTextChannel = channel.type === ChannelType.GuildText
277
+ if (!isThread && !isTextChannel) {
278
+ await command.reply({
279
+ content: 'This command can only be used in a text channel or thread.',
280
+ flags: SECURE_REPLY_FLAGS,
281
+ })
282
+ return
283
+ }
284
+
285
+ const resolved = await resolveWorkingDirectory({
286
+ channel: channel as TextChannel | ThreadChannel,
287
+ })
288
+ if (!resolved) {
289
+ await command.reply({
290
+ content: 'Could not determine project directory for this channel.',
291
+ flags: SECURE_REPLY_FLAGS,
292
+ })
293
+ return
294
+ }
295
+
296
+ await command.deferReply({ flags: SECURE_REPLY_FLAGS })
297
+
298
+ const sessionKey = channel.id
299
+ const existing = getActiveVscodeSession({ sessionKey })
300
+ if (existing) {
301
+ await command.editReply({
302
+ content:
303
+ `VS Code is already running for this thread. ` +
304
+ `This unique tunnel auto-stops after ${MAX_SESSION_MINUTES} minutes from startup.\n` +
305
+ `${existing.url}`,
306
+ })
307
+ return
308
+ }
309
+
310
+ try {
311
+ const session = await startVscode({
312
+ sessionKey,
313
+ startedBy: command.user.tag,
314
+ workingDirectory: resolved.workingDirectory,
315
+ })
316
+ await command.editReply({
317
+ content:
318
+ `VS Code started for \`${session.workingDirectory}\`. ` +
319
+ `This unique tunnel auto-stops after ${MAX_SESSION_MINUTES} minutes, so open it before it expires.\n` +
320
+ `${session.url}`,
321
+ })
322
+ } catch (error) {
323
+ logger.error('Failed to start VS Code:', error)
324
+ await command.editReply({
325
+ content: `Failed to start VS Code: ${error instanceof Error ? error.message : String(error)}`,
326
+ })
327
+ }
328
+ }
329
+
330
+ export function cleanupAllVscodeSessions(): void {
331
+ for (const sessionKey of activeSessions.keys()) {
332
+ stopVscode({ sessionKey })
333
+ }
334
+ }
335
+
336
+ function onProcessExit(): void {
337
+ cleanupAllVscodeSessions()
338
+ }
339
+
340
+ process.on('SIGINT', onProcessExit)
341
+ process.on('SIGTERM', onProcessExit)
342
+ process.on('exit', onProcessExit)
package/src/db.ts CHANGED
@@ -235,6 +235,7 @@ async function migrateSchema(prisma: PrismaClient): Promise<void> {
235
235
  // Also fix NULL worktree status rows that predate the required enum.
236
236
  const defensiveMigrations = [
237
237
  "UPDATE bot_tokens SET bot_mode = 'self_hosted' WHERE bot_mode = 'self-hosted'",
238
+ "UPDATE bot_tokens SET proxy_url = REPLACE(proxy_url, 'discord-gateway.kimaki.xyz', 'discord-gateway.kimaki.dev') WHERE bot_mode = 'gateway' AND proxy_url LIKE '%discord-gateway.kimaki.xyz%'",
238
239
  "UPDATE thread_worktrees SET status = 'pending' WHERE status IS NULL",
239
240
  ]
240
241
  for (const stmt of defensiveMigrations) {
@@ -488,6 +488,13 @@ export async function registerCommands({
488
488
  .setDescription(truncateCommandDescription('Stop screen sharing'))
489
489
  .setDMPermission(false)
490
490
  .toJSON(),
491
+ new SlashCommandBuilder()
492
+ .setName('vscode')
493
+ .setDescription(
494
+ truncateCommandDescription('Open VS Code in the browser for this project or worktree (auto-stops after 30 minutes)'),
495
+ )
496
+ .setDMPermission(false)
497
+ .toJSON(),
491
498
  ]
492
499
 
493
500
  // Dynamic commands are registered in priority order: agents → user commands → skills → MCP prompts.
@@ -6,7 +6,7 @@
6
6
  // Starts a digital-twin + local gateway-proxy binary, kills and restarts the proxy.
7
7
  //
8
8
  // Production mode (env vars):
9
- // GATEWAY_TEST_URL - production gateway WS+REST URL (e.g. wss://discord-gateway.kimaki.xyz)
9
+ // GATEWAY_TEST_URL - production gateway WS+REST URL (e.g. wss://discord-gateway.kimaki.dev)
10
10
  // GATEWAY_TEST_TOKEN - client token (clientId:secret)
11
11
  // GATEWAY_TEST_REDEPLOY - if "1", runs `fly deploy` between kill/restart instead of local binary
12
12
  //
@@ -15,7 +15,7 @@
15
15
  // pnpm test --run src/gateway-proxy-reconnect.e2e.test.ts
16
16
  //
17
17
  // # Against production (just connect + kill WS + wait for reconnect):
18
- // GATEWAY_TEST_URL=wss://discord-gateway.kimaki.xyz \
18
+ // GATEWAY_TEST_URL=wss://discord-gateway.kimaki.dev \
19
19
  // GATEWAY_TEST_TOKEN=myclientid:mysecret \
20
20
  // KIMAKI_TEST_LOGS=1 \
21
21
  // pnpm test --run src/gateway-proxy-reconnect.e2e.test.ts -t "production"
@@ -101,6 +101,7 @@ import {
101
101
  handleScreenshareCommand,
102
102
  handleScreenshareStopCommand,
103
103
  } from './commands/screenshare.js'
104
+ import { handleVscodeCommand } from './commands/vscode.js'
104
105
  import { handleModelVariantSelectMenu } from './commands/model.js'
105
106
  import {
106
107
  handleModelVariantCommand,
@@ -356,6 +357,10 @@ export function registerInteractionHandler({
356
357
  appId,
357
358
  })
358
359
  return
360
+
361
+ case 'vscode':
362
+ await handleVscodeCommand({ command: interaction, appId })
363
+ return
359
364
  }
360
365
 
361
366
  // Handle quick agent commands (ending with -agent suffix, but not the base /agent command)
@@ -23,7 +23,7 @@ describe('system-message', () => {
23
23
  }).replace(/`[^`]*\/kimaki\.log`/, '`<data-dir>/kimaki.log`'),
24
24
  ).toMatchInlineSnapshot(`
25
25
  "
26
- The user is reading your messages from inside Discord, via kimaki.xyz
26
+ The user is reading your messages from inside Discord, via kimaki.dev
27
27
 
28
28
  ## bash tool
29
29
 
@@ -92,39 +92,40 @@ describe('system-message', () => {
92
92
 
93
93
  To start a new thread/session in this channel pro-grammatically, run:
94
94
 
95
- kimaki send --channel chan_123 --prompt "your prompt here" --user "Tommy"
95
+ kimaki send --channel chan_123 --prompt "your prompt here" --agent <current_agent> --user "Tommy"
96
96
 
97
97
  You can use this to "spawn" parallel helper sessions like teammates: start new threads with focused prompts, then come back and collect the results.
98
+ Prefer passing the current agent with \`--agent <current_agent>\` so spawned or scheduled sessions keep the same agent unless you are intentionally switching. Replace \`<current_agent>\` with the value from the per-turn \`Current agent\` reminder.
98
99
 
99
100
  IMPORTANT: NEVER use \`--worktree\` unless the user explicitly asks for a worktree. Default to creating normal threads without worktrees.
100
101
 
101
102
  To send a prompt to an existing thread instead of creating a new one:
102
103
 
103
- kimaki send --thread <thread_id> --prompt "follow-up prompt"
104
+ kimaki send --thread <thread_id> --prompt "follow-up prompt" --agent <current_agent>
104
105
 
105
106
  Use this when you already have the Discord thread ID.
106
107
 
107
108
  To send to the thread associated with a known session:
108
109
 
109
- kimaki send --session <session_id> --prompt "follow-up prompt"
110
+ kimaki send --session <session_id> --prompt "follow-up prompt" --agent <current_agent>
110
111
 
111
112
  Use this when you have the OpenCode session ID.
112
113
 
113
114
  Use --notify-only to create a notification thread without starting an AI session:
114
115
 
115
- kimaki send --channel chan_123 --prompt "User cancelled subscription" --notify-only --user "Tommy"
116
+ kimaki send --channel chan_123 --prompt "User cancelled subscription" --notify-only --agent <current_agent> --user "Tommy"
116
117
 
117
118
  Use --user to add a specific Discord user to the new thread:
118
119
 
119
- kimaki send --channel chan_123 --prompt "Review the latest CI failure" --user "Tommy"
120
+ kimaki send --channel chan_123 --prompt "Review the latest CI failure" --agent <current_agent> --user "Tommy"
120
121
 
121
122
  Use --worktree to create a git worktree for the session (ONLY when the user explicitly asks for a worktree):
122
123
 
123
- kimaki send --channel chan_123 --prompt "Add dark mode support" --worktree dark-mode --user "Tommy"
124
+ kimaki send --channel chan_123 --prompt "Add dark mode support" --worktree dark-mode --agent <current_agent> --user "Tommy"
124
125
 
125
126
  Use --cwd to start a session in an existing git worktree directory (must be a worktree of the project):
126
127
 
127
- kimaki send --channel chan_123 --prompt "Continue work on feature" --cwd /path/to/existing-worktree --user "Tommy"
128
+ kimaki send --channel chan_123 --prompt "Continue work on feature" --cwd /path/to/existing-worktree --agent <current_agent> --user "Tommy"
128
129
 
129
130
  Important:
130
131
  - NEVER use \`--worktree\` unless the user explicitly requests a worktree. Most tasks should use normal threads without worktrees.
@@ -146,8 +147,8 @@ describe('system-message', () => {
146
147
 
147
148
  You can trigger registered opencode commands (slash commands, skills, MCP prompts) by starting the \`--prompt\` with \`/commandname\`:
148
149
 
149
- kimaki send --thread <thread_id> --prompt "/review fix the auth module"
150
- kimaki send --channel chan_123 --prompt "/build-cmd update dependencies" --user "Tommy"
150
+ kimaki send --thread <thread_id> --prompt "/review fix the auth module" --agent <current_agent>
151
+ kimaki send --channel chan_123 --prompt "/build-cmd update dependencies" --agent <current_agent> --user "Tommy"
151
152
 
152
153
  The command name must match a registered opencode command. If the command is not recognized, the prompt is sent as plain text to the model. This works for both new threads (\`--channel\`) and existing threads (\`--thread\`/\`--session\`).
153
154
 
@@ -157,14 +158,14 @@ describe('system-message', () => {
157
158
 
158
159
  You can also switch agents via \`kimaki send\`:
159
160
 
160
- kimaki send --thread <thread_id> --prompt "/<agentname>-agent"
161
+ kimaki send --thread <thread_id> --prompt "/<agentname>-agent" --agent <current_agent>
161
162
 
162
163
  ## scheduled sends and task management
163
164
 
164
165
  Use \`--send-at\` to schedule a one-time or recurring task:
165
166
 
166
- kimaki send --channel chan_123 --prompt "Reminder: review open PRs" --send-at "2026-03-01T09:00:00Z" --user "Tommy"
167
- kimaki send --channel chan_123 --prompt "Run weekly test suite and summarize failures" --send-at "0 9 * * 1" --user "Tommy"
167
+ kimaki send --channel chan_123 --prompt "Reminder: review open PRs" --send-at "2026-03-01T09:00:00Z" --agent <current_agent> --user "Tommy"
168
+ kimaki send --channel chan_123 --prompt "Run weekly test suite and summarize failures" --send-at "0 9 * * 1" --agent <current_agent> --user "Tommy"
168
169
 
169
170
  ALL scheduling is in UTC. Dates must be UTC ISO format ending with \`Z\`. Cron expressions also fire in UTC (e.g. \`0 9 * * 1\` means 9:00 UTC every Monday).
170
171
  When the user specifies a time without a timezone, ask them to confirm their timezone or the UTC equivalent. Never guess the user's timezone.
@@ -198,13 +199,13 @@ describe('system-message', () => {
198
199
 
199
200
  Use case patterns:
200
201
  - Reminder flows: create deadline reminders in this channel with one-time \`--send-at\`; mention only if action is required.
201
- - Proactive reminders: when you encounter time-sensitive information during your work (e.g. creating an API key that expires in 90 days, a certificate with an expiration date, a trial period ending, a deadline mentioned in code comments), proactively schedule a \`--notify-only\` reminder before the expiration so the user gets notified in time. For example, if you generate an API key expiring on 2026-06-01, schedule a reminder a few days before: \`kimaki send --channel chan_123 --prompt "Reminder: <@USER_ID> the API key created on 2026-03-01 expires on 2026-06-01. Renew it before it breaks production." --send-at "2026-05-28T09:00:00Z" --notify-only\`. Always tell the user you scheduled the reminder so they know.
202
+ - Proactive reminders: when you encounter time-sensitive information during your work (e.g. creating an API key that expires in 90 days, a certificate with an expiration date, a trial period ending, a deadline mentioned in code comments), proactively schedule a \`--notify-only\` reminder before the expiration so the user gets notified in time. For example, if you generate an API key expiring on 2026-06-01, schedule a reminder a few days before: \`kimaki send --channel chan_123 --prompt "Reminder: <@USER_ID> the API key created on 2026-03-01 expires on 2026-06-01. Renew it before it breaks production." --send-at "2026-05-28T09:00:00Z" --notify-only --agent <current_agent>\`. Always tell the user you scheduled the reminder so they know.
202
203
  - Weekly QA: schedule "run full test suite, inspect failures, post summary, and mention @username only when failures require review".
203
204
  - Weekly benchmark automation: schedule a benchmark prompt that runs model evals, writes JSON outputs in the repo, commits results, and mentions only for regressions.
204
205
  - Recurring maintenance: use cron \`--send-at\` for repetitive tasks like rotating secrets, checking dependency updates, running security audits, or cleaning up stale branches. Example: \`--send-at "0 9 1 * *"\` to run on the 1st of every month.
205
206
  - Thread reminders: when the user says "remind me about this in 2 hours" (or any duration), use \`--send-at\` with \`--thread\` to resurface the current thread. Compute the future UTC time and send a mention so Discord shows a notification:
206
207
 
207
- kimaki send --session ses_123 --prompt "Reminder: <@USER_ID> you asked to be reminded about this thread." --send-at "<future_UTC_time>" --notify-only
208
+ kimaki send --session ses_123 --prompt "Reminder: <@USER_ID> you asked to be reminded about this thread." --send-at "<future_UTC_time>" --notify-only --agent <current_agent>
208
209
 
209
210
  Replace \`<future_UTC_time>\` with the computed UTC ISO timestamp. The \`--notify-only\` flag creates just a notification message without starting a new AI session. The \`<@userId>\` mention ensures the user gets a Discord notification.
210
211
 
@@ -219,7 +220,7 @@ describe('system-message', () => {
219
220
  When the user asks to "create a worktree" or "make a worktree", they mean you should use the kimaki CLI to create it. Do NOT use raw \`git worktree add\` commands. Instead use:
220
221
 
221
222
  \`\`\`bash
222
- kimaki send --channel chan_123 --prompt "your task description" --worktree worktree-name --user "Tommy"
223
+ kimaki send --channel chan_123 --prompt "your task description" --worktree worktree-name --agent <current_agent> --user "Tommy"
223
224
  \`\`\`
224
225
 
225
226
  This creates a new Discord thread with an isolated git worktree and starts a session in it. The worktree name should be kebab-case and descriptive of the task.
@@ -235,7 +236,7 @@ describe('system-message', () => {
235
236
  Use \`--cwd\` to start a session in an existing git worktree directory instead of creating a new one:
236
237
 
237
238
  \`\`\`bash
238
- kimaki send --channel chan_123 --prompt "Continue work on feature X" --cwd /path/to/existing-worktree --user "Tommy"
239
+ kimaki send --channel chan_123 --prompt "Continue work on feature X" --cwd /path/to/existing-worktree --agent <current_agent> --user "Tommy"
239
240
  \`\`\`
240
241
 
241
242
  The path must be a git worktree of the project (validated via \`git worktree list\`). The session resolves to the correct project channel but uses the worktree as its working directory. Use \`--worktree\` to create a new worktree, \`--cwd\` to reuse an existing one.
@@ -249,7 +250,7 @@ describe('system-message', () => {
249
250
  When you are approaching the **context window limit** or the user explicitly asks to **handoff to a new thread**, use the \`kimaki send\` command to start a fresh session with context:
250
251
 
251
252
  \`\`\`bash
252
- kimaki send --channel chan_123 --prompt "Continuing from previous session: <summary of current task and state>" --user "Tommy"
253
+ kimaki send --channel chan_123 --prompt "Continuing from previous session: <summary of current task and state>" --agent <current_agent> --user "Tommy"
253
254
  \`\`\`
254
255
 
255
256
  The command automatically handles long prompts (over 2000 chars) by sending them as file attachments.
@@ -307,10 +308,10 @@ describe('system-message', () => {
307
308
 
308
309
  \`\`\`bash
309
310
  # Send to a specific channel
310
- kimaki send --channel <channel_id> --prompt "Plan how to update the API client to v2"
311
+ kimaki send --channel <channel_id> --prompt "Plan how to update the API client to v2" --agent <current_agent>
311
312
 
312
313
  # Or use --project to resolve from directory
313
- kimaki send --project /path/to/other-repo --prompt "Plan how to bump version to 1.2.0"
314
+ kimaki send --project /path/to/other-repo --prompt "Plan how to bump version to 1.2.0" --agent <current_agent>
314
315
  \`\`\`
315
316
 
316
317
  When sending prompts to other projects, always ask the agent to plan first, never build upfront. The prompt should start with "Plan how to ..." so the user can review before greenlighting implementation.
@@ -333,10 +334,10 @@ describe('system-message', () => {
333
334
 
334
335
  \`\`\`bash
335
336
  # Start a session and wait for it to finish
336
- kimaki send --channel <channel_id> --prompt "Fix the auth bug" --wait
337
+ kimaki send --channel <channel_id> --prompt "Fix the auth bug" --wait --agent <current_agent>
337
338
 
338
339
  # Send to an existing thread and wait
339
- kimaki send --thread <thread_id> --prompt "Run the tests" --wait
340
+ kimaki send --thread <thread_id> --prompt "Run the tests" --wait --agent <current_agent>
340
341
  \`\`\`
341
342
 
342
343
  The command exits with the session markdown on stdout once the model finishes responding.