@otto-assistant/bridge 0.4.96 → 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.
Files changed (44) hide show
  1. package/dist/agent-model.e2e.test.js +7 -1
  2. package/dist/anthropic-account-identity.js +62 -0
  3. package/dist/anthropic-account-identity.test.js +38 -0
  4. package/dist/anthropic-auth-plugin.js +72 -12
  5. package/dist/anthropic-auth-state.js +28 -3
  6. package/dist/{anthropic-auth-plugin.test.js → anthropic-auth-state.test.js} +21 -2
  7. package/dist/cli-parsing.test.js +12 -9
  8. package/dist/cli-send-thread.e2e.test.js +4 -7
  9. package/dist/cli.js +25 -12
  10. package/dist/commands/screenshare.js +1 -1
  11. package/dist/commands/screenshare.test.js +2 -2
  12. package/dist/commands/vscode.js +269 -0
  13. package/dist/db.js +1 -0
  14. package/dist/discord-command-registration.js +7 -2
  15. package/dist/gateway-proxy-reconnect.e2e.test.js +2 -2
  16. package/dist/interaction-handler.js +4 -0
  17. package/dist/system-message.js +24 -23
  18. package/dist/system-message.test.js +24 -23
  19. package/dist/system-prompt-drift-plugin.js +41 -11
  20. package/dist/utils.js +1 -1
  21. package/dist/worktrees.js +0 -33
  22. package/package.json +1 -1
  23. package/src/agent-model.e2e.test.ts +8 -1
  24. package/src/anthropic-account-identity.test.ts +52 -0
  25. package/src/anthropic-account-identity.ts +77 -0
  26. package/src/anthropic-auth-plugin.ts +82 -12
  27. package/src/{anthropic-auth-plugin.test.ts → anthropic-auth-state.test.ts} +23 -1
  28. package/src/anthropic-auth-state.ts +36 -3
  29. package/src/cli-parsing.test.ts +16 -9
  30. package/src/cli-send-thread.e2e.test.ts +6 -7
  31. package/src/cli.ts +31 -13
  32. package/src/commands/screenshare.test.ts +2 -2
  33. package/src/commands/screenshare.ts +1 -1
  34. package/src/commands/vscode.ts +342 -0
  35. package/src/db.ts +1 -0
  36. package/src/discord-command-registration.ts +9 -2
  37. package/src/gateway-proxy-reconnect.e2e.test.ts +2 -2
  38. package/src/interaction-handler.ts +5 -0
  39. package/src/system-message.test.ts +24 -23
  40. package/src/system-message.ts +24 -23
  41. package/src/system-prompt-drift-plugin.ts +48 -12
  42. package/src/utils.ts +1 -1
  43. package/src/worktrees.test.ts +1 -0
  44. package/src/worktrees.ts +1 -47
@@ -0,0 +1,269 @@
1
+ import crypto from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import net from 'node:net';
4
+ import { ChannelType, MessageFlags, } from 'discord.js';
5
+ import { TunnelClient } from 'traforo/client';
6
+ import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
7
+ import { createLogger } from '../logger.js';
8
+ const logger = createLogger('VSCODE');
9
+ const SECURE_REPLY_FLAGS = MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS;
10
+ const MAX_SESSION_MINUTES = 30;
11
+ const MAX_SESSION_MS = MAX_SESSION_MINUTES * 60 * 1000;
12
+ const TUNNEL_BASE_DOMAIN = 'kimaki.dev';
13
+ const TUNNEL_ID_BYTES = 16;
14
+ const READY_TIMEOUT_MS = 60_000;
15
+ const LOCAL_HOST = '127.0.0.1';
16
+ const activeSessions = new Map();
17
+ export function createVscodeTunnelId() {
18
+ return crypto.randomBytes(TUNNEL_ID_BYTES).toString('hex');
19
+ }
20
+ export function buildCoderaftArgs({ port, workingDirectory, }) {
21
+ return [
22
+ 'coderaft',
23
+ '--port',
24
+ String(port),
25
+ '--host',
26
+ LOCAL_HOST,
27
+ '--without-connection-token',
28
+ '--disable-workspace-trust',
29
+ '--default-folder',
30
+ workingDirectory,
31
+ ];
32
+ }
33
+ function createPortWaiter({ port, process: proc, timeoutMs, }) {
34
+ return new Promise((resolve, reject) => {
35
+ const maxAttempts = Math.ceil(timeoutMs / 100);
36
+ let attempts = 0;
37
+ const check = () => {
38
+ if (proc.exitCode !== null) {
39
+ reject(new Error(`coderaft exited with code ${proc.exitCode} before becoming ready`));
40
+ return;
41
+ }
42
+ const socket = net.createConnection(port, LOCAL_HOST);
43
+ socket.on('connect', () => {
44
+ socket.destroy();
45
+ resolve();
46
+ });
47
+ socket.on('error', () => {
48
+ socket.destroy();
49
+ attempts += 1;
50
+ if (attempts >= maxAttempts) {
51
+ reject(new Error(`Port ${port} not reachable after ${timeoutMs}ms`));
52
+ return;
53
+ }
54
+ setTimeout(check, 100);
55
+ });
56
+ };
57
+ check();
58
+ });
59
+ }
60
+ function getAvailablePort() {
61
+ return new Promise((resolve, reject) => {
62
+ const server = net.createServer();
63
+ server.on('error', reject);
64
+ server.listen(0, LOCAL_HOST, () => {
65
+ const address = server.address();
66
+ if (!address || typeof address === 'string') {
67
+ server.close(() => {
68
+ reject(new Error('Failed to resolve an available port'));
69
+ });
70
+ return;
71
+ }
72
+ const port = address.port;
73
+ server.close((error) => {
74
+ if (error) {
75
+ reject(error);
76
+ return;
77
+ }
78
+ resolve(port);
79
+ });
80
+ });
81
+ });
82
+ }
83
+ function cleanupSession(session) {
84
+ clearTimeout(session.timeoutTimer);
85
+ try {
86
+ session.tunnelClient.close();
87
+ }
88
+ catch { }
89
+ if (session.coderaftProcess.exitCode === null) {
90
+ try {
91
+ session.coderaftProcess.kill('SIGTERM');
92
+ }
93
+ catch { }
94
+ }
95
+ }
96
+ export function getActiveVscodeSession({ sessionKey }) {
97
+ return activeSessions.get(sessionKey);
98
+ }
99
+ export function stopVscode({ sessionKey }) {
100
+ const session = activeSessions.get(sessionKey);
101
+ if (!session) {
102
+ return false;
103
+ }
104
+ activeSessions.delete(sessionKey);
105
+ cleanupSession(session);
106
+ logger.log(`VS Code stopped (key: ${sessionKey})`);
107
+ return true;
108
+ }
109
+ export async function startVscode({ sessionKey, startedBy, workingDirectory, }) {
110
+ const existing = activeSessions.get(sessionKey);
111
+ if (existing) {
112
+ return existing;
113
+ }
114
+ const port = await getAvailablePort();
115
+ const tunnelId = createVscodeTunnelId();
116
+ const args = buildCoderaftArgs({
117
+ port,
118
+ workingDirectory,
119
+ });
120
+ const coderaftProcess = spawn('bunx', args, {
121
+ cwd: workingDirectory,
122
+ stdio: ['ignore', 'pipe', 'pipe'],
123
+ env: {
124
+ ...process.env,
125
+ PORT: String(port),
126
+ },
127
+ });
128
+ coderaftProcess.stdout?.on('data', (data) => {
129
+ logger.log(data.toString().trim());
130
+ });
131
+ coderaftProcess.stderr?.on('data', (data) => {
132
+ logger.error(data.toString().trim());
133
+ });
134
+ try {
135
+ await createPortWaiter({
136
+ port,
137
+ process: coderaftProcess,
138
+ timeoutMs: READY_TIMEOUT_MS,
139
+ });
140
+ }
141
+ catch (error) {
142
+ if (coderaftProcess.exitCode === null) {
143
+ coderaftProcess.kill('SIGTERM');
144
+ }
145
+ throw error;
146
+ }
147
+ const tunnelClient = new TunnelClient({
148
+ localPort: port,
149
+ localHost: LOCAL_HOST,
150
+ tunnelId,
151
+ baseDomain: TUNNEL_BASE_DOMAIN,
152
+ });
153
+ try {
154
+ await Promise.race([
155
+ tunnelClient.connect(),
156
+ new Promise((_, reject) => {
157
+ setTimeout(() => {
158
+ reject(new Error('Tunnel connection timed out after 15s'));
159
+ }, 15_000);
160
+ }),
161
+ ]);
162
+ }
163
+ catch (error) {
164
+ tunnelClient.close();
165
+ if (coderaftProcess.exitCode === null) {
166
+ coderaftProcess.kill('SIGTERM');
167
+ }
168
+ throw error;
169
+ }
170
+ const url = tunnelClient.url;
171
+ const timeoutTimer = setTimeout(() => {
172
+ logger.log(`VS Code auto-stopped after ${MAX_SESSION_MINUTES} minutes (key: ${sessionKey})`);
173
+ stopVscode({ sessionKey });
174
+ }, MAX_SESSION_MS);
175
+ timeoutTimer.unref();
176
+ const session = {
177
+ coderaftProcess,
178
+ tunnelClient,
179
+ url,
180
+ workingDirectory,
181
+ startedBy,
182
+ startedAt: Date.now(),
183
+ timeoutTimer,
184
+ };
185
+ coderaftProcess.once('exit', (code, signal) => {
186
+ const current = activeSessions.get(sessionKey);
187
+ if (current !== session) {
188
+ return;
189
+ }
190
+ logger.log(`VS Code process exited (key: ${sessionKey}, code: ${code}, signal: ${signal ?? 'none'})`);
191
+ stopVscode({ sessionKey });
192
+ });
193
+ activeSessions.set(sessionKey, session);
194
+ logger.log(`VS Code started by ${startedBy}: ${url}`);
195
+ return session;
196
+ }
197
+ export async function handleVscodeCommand({ command, }) {
198
+ const channel = command.channel;
199
+ if (!channel) {
200
+ await command.reply({
201
+ content: 'This command can only be used in a channel.',
202
+ flags: SECURE_REPLY_FLAGS,
203
+ });
204
+ return;
205
+ }
206
+ const isThread = [
207
+ ChannelType.PublicThread,
208
+ ChannelType.PrivateThread,
209
+ ChannelType.AnnouncementThread,
210
+ ].includes(channel.type);
211
+ const isTextChannel = channel.type === ChannelType.GuildText;
212
+ if (!isThread && !isTextChannel) {
213
+ await command.reply({
214
+ content: 'This command can only be used in a text channel or thread.',
215
+ flags: SECURE_REPLY_FLAGS,
216
+ });
217
+ return;
218
+ }
219
+ const resolved = await resolveWorkingDirectory({
220
+ channel: channel,
221
+ });
222
+ if (!resolved) {
223
+ await command.reply({
224
+ content: 'Could not determine project directory for this channel.',
225
+ flags: SECURE_REPLY_FLAGS,
226
+ });
227
+ return;
228
+ }
229
+ await command.deferReply({ flags: SECURE_REPLY_FLAGS });
230
+ const sessionKey = channel.id;
231
+ const existing = getActiveVscodeSession({ sessionKey });
232
+ if (existing) {
233
+ await command.editReply({
234
+ content: `VS Code is already running for this thread. ` +
235
+ `This unique tunnel auto-stops after ${MAX_SESSION_MINUTES} minutes from startup.\n` +
236
+ `${existing.url}`,
237
+ });
238
+ return;
239
+ }
240
+ try {
241
+ const session = await startVscode({
242
+ sessionKey,
243
+ startedBy: command.user.tag,
244
+ workingDirectory: resolved.workingDirectory,
245
+ });
246
+ await command.editReply({
247
+ content: `VS Code started for \`${session.workingDirectory}\`. ` +
248
+ `This unique tunnel auto-stops after ${MAX_SESSION_MINUTES} minutes, so open it before it expires.\n` +
249
+ `${session.url}`,
250
+ });
251
+ }
252
+ catch (error) {
253
+ logger.error('Failed to start VS Code:', error);
254
+ await command.editReply({
255
+ content: `Failed to start VS Code: ${error instanceof Error ? error.message : String(error)}`,
256
+ });
257
+ }
258
+ }
259
+ export function cleanupAllVscodeSessions() {
260
+ for (const sessionKey of activeSessions.keys()) {
261
+ stopVscode({ sessionKey });
262
+ }
263
+ }
264
+ function onProcessExit() {
265
+ cleanupAllVscodeSessions();
266
+ }
267
+ process.on('SIGINT', onProcessExit);
268
+ process.on('SIGTERM', onProcessExit);
269
+ process.on('exit', onProcessExit);
package/dist/db.js CHANGED
@@ -201,6 +201,7 @@ async function migrateSchema(prisma) {
201
201
  // Also fix NULL worktree status rows that predate the required enum.
202
202
  const defensiveMigrations = [
203
203
  "UPDATE bot_tokens SET bot_mode = 'self_hosted' WHERE bot_mode = 'self-hosted'",
204
+ "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%'",
204
205
  "UPDATE thread_worktrees SET status = 'pending' WHERE status IS NULL",
205
206
  ];
206
207
  for (const stmt of defensiveMigrations) {
@@ -110,7 +110,7 @@ export async function registerCommands({ token, appId, guildIds, userCommands =
110
110
  .toJSON(),
111
111
  new SlashCommandBuilder()
112
112
  .setName('new-worktree')
113
- .setDescription(truncateCommandDescription('Create a git worktree branch from origin/HEAD (or main). Optionally pick a base branch.'))
113
+ .setDescription(truncateCommandDescription('Create a git worktree branch from HEAD by default. Optionally pick a base branch.'))
114
114
  .addStringOption((option) => {
115
115
  option
116
116
  .setName('name')
@@ -121,7 +121,7 @@ export async function registerCommands({ token, appId, guildIds, userCommands =
121
121
  .addStringOption((option) => {
122
122
  option
123
123
  .setName('base-branch')
124
- .setDescription(truncateCommandDescription('Branch to create the worktree from (default: origin/HEAD or main)'))
124
+ .setDescription(truncateCommandDescription('Branch to create the worktree from (default: HEAD)'))
125
125
  .setRequired(false)
126
126
  .setAutocomplete(true);
127
127
  return option;
@@ -365,6 +365,11 @@ export async function registerCommands({ token, appId, guildIds, userCommands =
365
365
  .setDescription(truncateCommandDescription('Stop screen sharing'))
366
366
  .setDMPermission(false)
367
367
  .toJSON(),
368
+ new SlashCommandBuilder()
369
+ .setName('vscode')
370
+ .setDescription(truncateCommandDescription('Open VS Code in the browser for this project or worktree (auto-stops after 30 minutes)'))
371
+ .setDMPermission(false)
372
+ .toJSON(),
368
373
  ];
369
374
  // Dynamic commands are registered in priority order: agents → user commands → skills → MCP prompts.
370
375
  // This ordering matters because we slice to MAX_DISCORD_COMMANDS (100) at the end,
@@ -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"
@@ -39,6 +39,7 @@ import { handleSessionIdCommand } from './commands/session-id.js';
39
39
  import { handleUpgradeAndRestartCommand } from './commands/upgrade.js';
40
40
  import { handleMcpCommand, handleMcpSelectMenu } from './commands/mcp.js';
41
41
  import { handleScreenshareCommand, handleScreenshareStopCommand, } from './commands/screenshare.js';
42
+ import { handleVscodeCommand } from './commands/vscode.js';
42
43
  import { handleModelVariantSelectMenu } from './commands/model.js';
43
44
  import { handleModelVariantCommand, handleVariantQuickSelectMenu, handleVariantScopeSelectMenu, } from './commands/model-variant.js';
44
45
  import { hasKimakiBotPermission } from './discord-utils.js';
@@ -224,6 +225,9 @@ export function registerInteractionHandler({ discordClient, appId, }) {
224
225
  appId,
225
226
  });
226
227
  return;
228
+ case 'vscode':
229
+ await handleVscodeCommand({ command: interaction, appId });
230
+ return;
227
231
  }
228
232
  // Handle quick agent commands (ending with -agent suffix, but not the base /agent command)
229
233
  if (interaction.commandName.endsWith('-agent') &&
@@ -255,7 +255,7 @@ export function getOpencodeSystemMessage({ sessionId, channelId, guildId, thread
255
255
  .join('\n')}`
256
256
  : '';
257
257
  return `
258
- The user is reading your messages from inside Discord, via kimaki.xyz
258
+ The user is reading your messages from inside Discord, via kimaki.dev
259
259
 
260
260
  ## bash tool
261
261
 
@@ -322,39 +322,40 @@ ${channelId
322
322
 
323
323
  To start a new thread/session in this channel pro-grammatically, run:
324
324
 
325
- kimaki send --channel ${channelId} --prompt "your prompt here"${userArg}
325
+ kimaki send --channel ${channelId} --prompt "your prompt here" --agent <current_agent>${userArg}
326
326
 
327
327
  You can use this to "spawn" parallel helper sessions like teammates: start new threads with focused prompts, then come back and collect the results.
328
+ 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.
328
329
 
329
330
  IMPORTANT: NEVER use \`--worktree\` unless the user explicitly asks for a worktree. Default to creating normal threads without worktrees.
330
331
 
331
332
  To send a prompt to an existing thread instead of creating a new one:
332
333
 
333
- kimaki send --thread <thread_id> --prompt "follow-up prompt"
334
+ kimaki send --thread <thread_id> --prompt "follow-up prompt" --agent <current_agent>
334
335
 
335
336
  Use this when you already have the Discord thread ID.
336
337
 
337
338
  To send to the thread associated with a known session:
338
339
 
339
- kimaki send --session <session_id> --prompt "follow-up prompt"
340
+ kimaki send --session <session_id> --prompt "follow-up prompt" --agent <current_agent>
340
341
 
341
342
  Use this when you have the OpenCode session ID.
342
343
 
343
344
  Use --notify-only to create a notification thread without starting an AI session:
344
345
 
345
- kimaki send --channel ${channelId} --prompt "User cancelled subscription" --notify-only${userArg}
346
+ kimaki send --channel ${channelId} --prompt "User cancelled subscription" --notify-only --agent <current_agent>${userArg}
346
347
 
347
348
  Use --user to add a specific Discord user to the new thread:
348
349
 
349
- kimaki send --channel ${channelId} --prompt "Review the latest CI failure"${userArg}
350
+ kimaki send --channel ${channelId} --prompt "Review the latest CI failure" --agent <current_agent>${userArg}
350
351
 
351
352
  Use --worktree to create a git worktree for the session (ONLY when the user explicitly asks for a worktree):
352
353
 
353
- kimaki send --channel ${channelId} --prompt "Add dark mode support" --worktree dark-mode${userArg}
354
+ kimaki send --channel ${channelId} --prompt "Add dark mode support" --worktree dark-mode --agent <current_agent>${userArg}
354
355
 
355
356
  Use --cwd to start a session in an existing git worktree directory (must be a worktree of the project):
356
357
 
357
- kimaki send --channel ${channelId} --prompt "Continue work on feature" --cwd /path/to/existing-worktree${userArg}
358
+ kimaki send --channel ${channelId} --prompt "Continue work on feature" --cwd /path/to/existing-worktree --agent <current_agent>${userArg}
358
359
 
359
360
  Important:
360
361
  - NEVER use \`--worktree\` unless the user explicitly requests a worktree. Most tasks should use normal threads without worktrees.
@@ -372,8 +373,8 @@ ${availableAgentsContext}
372
373
 
373
374
  You can trigger registered opencode commands (slash commands, skills, MCP prompts) by starting the \`--prompt\` with \`/commandname\`:
374
375
 
375
- kimaki send --thread <thread_id> --prompt "/review fix the auth module"
376
- kimaki send --channel ${channelId} --prompt "/build-cmd update dependencies"${userArg}
376
+ kimaki send --thread <thread_id> --prompt "/review fix the auth module" --agent <current_agent>
377
+ kimaki send --channel ${channelId} --prompt "/build-cmd update dependencies" --agent <current_agent>${userArg}
377
378
 
378
379
  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\`).
379
380
 
@@ -383,14 +384,14 @@ The user can switch the active agent mid-session using the Discord slash command
383
384
 
384
385
  You can also switch agents via \`kimaki send\`:
385
386
 
386
- kimaki send --thread <thread_id> --prompt "/<agentname>-agent"
387
+ kimaki send --thread <thread_id> --prompt "/<agentname>-agent" --agent <current_agent>
387
388
 
388
389
  ## scheduled sends and task management
389
390
 
390
391
  Use \`--send-at\` to schedule a one-time or recurring task:
391
392
 
392
- kimaki send --channel ${channelId} --prompt "Reminder: review open PRs" --send-at "2026-03-01T09:00:00Z"${userArg}
393
- kimaki send --channel ${channelId} --prompt "Run weekly test suite and summarize failures" --send-at "0 9 * * 1"${userArg}
393
+ kimaki send --channel ${channelId} --prompt "Reminder: review open PRs" --send-at "2026-03-01T09:00:00Z" --agent <current_agent>${userArg}
394
+ kimaki send --channel ${channelId} --prompt "Run weekly test suite and summarize failures" --send-at "0 9 * * 1" --agent <current_agent>${userArg}
394
395
 
395
396
  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).
396
397
  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.
@@ -424,13 +425,13 @@ kimaki task delete <id>
424
425
 
425
426
  Use case patterns:
426
427
  - Reminder flows: create deadline reminders in this channel with one-time \`--send-at\`; mention only if action is required.
427
- - 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 ${channelId} --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.
428
+ - 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 ${channelId} --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.
428
429
  - Weekly QA: schedule "run full test suite, inspect failures, post summary, and mention @username only when failures require review".
429
430
  - Weekly benchmark automation: schedule a benchmark prompt that runs model evals, writes JSON outputs in the repo, commits results, and mentions only for regressions.
430
431
  - 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.
431
432
  - 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:
432
433
 
433
- kimaki send --session ${sessionId} --prompt "Reminder: <@USER_ID> you asked to be reminded about this thread." --send-at "<future_UTC_time>" --notify-only
434
+ kimaki send --session ${sessionId} --prompt "Reminder: <@USER_ID> you asked to be reminded about this thread." --send-at "<future_UTC_time>" --notify-only --agent <current_agent>
434
435
 
435
436
  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.
436
437
 
@@ -445,12 +446,12 @@ ONLY create worktrees when the user explicitly asks for one. Never proactively u
445
446
  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:
446
447
 
447
448
  \`\`\`bash
448
- kimaki send --channel ${channelId} --prompt "your task description" --worktree worktree-name${userArg}
449
+ kimaki send --channel ${channelId} --prompt "your task description" --worktree worktree-name --agent <current_agent>${userArg}
449
450
  \`\`\`
450
451
 
451
452
  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.
452
453
 
453
- By default, worktrees are created from \`origin/HEAD\` (the remote's default branch). To change the base branch for a project, the user can run \`git remote set-head origin <branch>\` in the project directory. For example, \`git remote set-head origin dev\` makes all new worktrees branch off \`origin/dev\` instead of \`origin/main\`.
454
+ By default, worktrees are created from \`HEAD\`, which means whatever commit or branch the current checkout is on. If you want a different base, pass \`--base-branch\` or use the slash command option explicitly.
454
455
 
455
456
  Critical recursion guard:
456
457
  - If you already are in a worktree thread, do not create another worktree unless the user explicitly asks for a nested worktree.
@@ -461,7 +462,7 @@ Critical recursion guard:
461
462
  Use \`--cwd\` to start a session in an existing git worktree directory instead of creating a new one:
462
463
 
463
464
  \`\`\`bash
464
- kimaki send --channel ${channelId} --prompt "Continue work on feature X" --cwd /path/to/existing-worktree${userArg}
465
+ kimaki send --channel ${channelId} --prompt "Continue work on feature X" --cwd /path/to/existing-worktree --agent <current_agent>${userArg}
465
466
  \`\`\`
466
467
 
467
468
  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.
@@ -475,7 +476,7 @@ This is useful for automation (cron jobs, GitHub webhooks, n8n, etc.)
475
476
  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:
476
477
 
477
478
  \`\`\`bash
478
- kimaki send --channel ${channelId} --prompt "Continuing from previous session: <summary of current task and state>"${userArg}
479
+ kimaki send --channel ${channelId} --prompt "Continuing from previous session: <summary of current task and state>" --agent <current_agent>${userArg}
479
480
  \`\`\`
480
481
 
481
482
  The command automatically handles long prompts (over 2000 chars) by sending them as file attachments.
@@ -533,10 +534,10 @@ To send a task to another project:
533
534
 
534
535
  \`\`\`bash
535
536
  # Send to a specific channel
536
- kimaki send --channel <channel_id> --prompt "Plan how to update the API client to v2"
537
+ kimaki send --channel <channel_id> --prompt "Plan how to update the API client to v2" --agent <current_agent>
537
538
 
538
539
  # Or use --project to resolve from directory
539
- kimaki send --project /path/to/other-repo --prompt "Plan how to bump version to 1.2.0"
540
+ kimaki send --project /path/to/other-repo --prompt "Plan how to bump version to 1.2.0" --agent <current_agent>
540
541
  \`\`\`
541
542
 
542
543
  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.
@@ -559,10 +560,10 @@ If your Bash tool timeout triggers anyway, fall back to reading the session outp
559
560
 
560
561
  \`\`\`bash
561
562
  # Start a session and wait for it to finish
562
- kimaki send --channel <channel_id> --prompt "Fix the auth bug" --wait
563
+ kimaki send --channel <channel_id> --prompt "Fix the auth bug" --wait --agent <current_agent>
563
564
 
564
565
  # Send to an existing thread and wait
565
- kimaki send --thread <thread_id> --prompt "Run the tests" --wait
566
+ kimaki send --thread <thread_id> --prompt "Run the tests" --wait --agent <current_agent>
566
567
  \`\`\`
567
568
 
568
569
  The command exits with the session markdown on stdout once the model finishes responding.