@otto-assistant/bridge 0.4.92 → 0.4.93

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.
@@ -735,7 +735,8 @@ describe('agent model resolution', () => {
735
735
  --- from: assistant (TestBot)
736
736
  ⬥ ok
737
737
  *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
738
- Switched to **plan** agent for this session next messages (was **test-agent**)
738
+ Switched to **plan** agent for this session (was **test-agent**)
739
+ The agent will change on the next message.
739
740
  --- from: user (agent-model-tester)
740
741
  Reply with exactly: after-switch-msg
741
742
  --- from: assistant (TestBot)
@@ -694,6 +694,13 @@ const AnthropicAuthPlugin = async ({ client }) => {
694
694
  if (shouldRotateAuth(response.status, bodyText)) {
695
695
  const rotated = await rotateAnthropicAccount(freshAuth, client);
696
696
  if (rotated) {
697
+ // Show toast notification so Discord thread shows the rotation
698
+ client.tui.showToast({
699
+ body: {
700
+ message: `Switching from account ${rotated.fromLabel} to account ${rotated.toLabel}`,
701
+ variant: 'info',
702
+ },
703
+ }).catch(() => { });
697
704
  const retryAuth = await getFreshOAuth(getAuth, client);
698
705
  if (retryAuth) {
699
706
  response = await runRequest(retryAuth);
@@ -67,7 +67,13 @@ describe('rotateAnthropicAccount', () => {
67
67
  const rotated = await rotateAnthropicAccount(firstAccount, client);
68
68
  const store = await loadAccountStore();
69
69
  const authJson = JSON.parse(await readFile(authFilePath(), 'utf8'));
70
- expect(rotated).toMatchObject({ refresh: 'refresh-second' });
70
+ expect(rotated).toMatchObject({
71
+ auth: { refresh: 'refresh-second' },
72
+ fromLabel: '#1 (refresh-...irst)',
73
+ toLabel: '#2 (refresh-...cond)',
74
+ fromIndex: 0,
75
+ toIndex: 1,
76
+ });
71
77
  expect(store.activeIndex).toBe(1);
72
78
  expect(authJson.anthropic?.refresh).toBe('refresh-second');
73
79
  expect(authSetCalls).toEqual([
@@ -94,6 +94,12 @@ export async function loadAccountStore() {
94
94
  export async function saveAccountStore(store) {
95
95
  await writeJson(accountsFilePath(), normalizeAccountStore(store));
96
96
  }
97
+ /** Short label for an account: first 8 + last 4 chars of refresh token. */
98
+ export function accountLabel(account, index) {
99
+ const r = account.refresh;
100
+ const short = r.length > 12 ? `${r.slice(0, 8)}...${r.slice(-4)}` : r;
101
+ return index !== undefined ? `#${index + 1} (${short})` : short;
102
+ }
97
103
  function findCurrentAccountIndex(store, auth) {
98
104
  if (!store.accounts.length)
99
105
  return 0;
@@ -165,10 +171,14 @@ export async function rotateAnthropicAccount(auth, client) {
165
171
  if (store.accounts.length < 2)
166
172
  return undefined;
167
173
  const currentIndex = findCurrentAccountIndex(store, auth);
174
+ const currentAccount = store.accounts[currentIndex];
168
175
  const nextIndex = (currentIndex + 1) % store.accounts.length;
169
176
  const nextAccount = store.accounts[nextIndex];
170
177
  if (!nextAccount)
171
178
  return undefined;
179
+ const fromLabel = currentAccount
180
+ ? accountLabel(currentAccount, currentIndex)
181
+ : accountLabel(auth, currentIndex);
172
182
  nextAccount.lastUsed = Date.now();
173
183
  store.activeIndex = nextIndex;
174
184
  await saveAccountStore(store);
@@ -179,7 +189,13 @@ export async function rotateAnthropicAccount(auth, client) {
179
189
  expires: nextAccount.expires,
180
190
  };
181
191
  await setAnthropicAuth(nextAuth, client);
182
- return nextAuth;
192
+ return {
193
+ auth: nextAuth,
194
+ fromLabel,
195
+ toLabel: accountLabel(nextAccount, nextIndex),
196
+ fromIndex: currentIndex,
197
+ toIndex: nextIndex,
198
+ };
183
199
  });
184
200
  }
185
201
  export async function removeAccount(index) {
package/dist/cli.js CHANGED
@@ -32,7 +32,7 @@ import { backgroundUpgradeKimaki, upgrade, getCurrentVersion, } from './upgrade.
32
32
  import { startHranaServer } from './hrana-server.js';
33
33
  import { startIpcPolling, stopIpcPolling } from './ipc-polling.js';
34
34
  import { getPromptPreview, parseSendAtValue, parseScheduledTaskPayload, serializeScheduledTaskPayload, } from './task-schedule.js';
35
- import { accountsFilePath, loadAccountStore, removeAccount, } from './anthropic-auth-state.js';
35
+ import { accountLabel, accountsFilePath, loadAccountStore, removeAccount, } from './anthropic-auth-state.js';
36
36
  const cliLogger = createLogger(LogPrefix.CLI);
37
37
  // Gateway bot mode constants.
38
38
  // KIMAKI_GATEWAY_APP_ID is the Discord Application ID of the gateway bot.
@@ -2234,8 +2234,7 @@ cli
2234
2234
  }
2235
2235
  store.accounts.forEach((account, index) => {
2236
2236
  const active = index === store.activeIndex ? '*' : ' ';
2237
- const label = `${account.refresh.slice(0, 8)}...${account.refresh.slice(-4)}`;
2238
- console.log(`${active} ${index + 1}. ${label}`);
2237
+ console.log(`${active} ${index + 1}. ${accountLabel(account)}`);
2239
2238
  });
2240
2239
  process.exit(0);
2241
2240
  });
@@ -255,7 +255,7 @@ export async function handleAgentSelectMenu(interaction) {
255
255
  await setAgentForContext({ context, agentName: selectedAgent });
256
256
  if (context.isThread && context.sessionId) {
257
257
  await interaction.editReply({
258
- content: `Agent preference set for this session next messages: **${selectedAgent}**`,
258
+ content: `Agent preference set for this session: **${selectedAgent}**\nThe agent will change on the next message.`,
259
259
  components: [],
260
260
  });
261
261
  }
@@ -317,7 +317,7 @@ export async function handleQuickAgentCommand({ command, appId, }) {
317
317
  : '';
318
318
  if (context.isThread && context.sessionId) {
319
319
  await command.editReply({
320
- content: `Switched to **${resolvedAgentName}** agent for this session next messages${previousText}`,
320
+ content: `Switched to **${resolvedAgentName}** agent for this session${previousText}\nThe agent will change on the next message.`,
321
321
  });
322
322
  }
323
323
  else {
@@ -103,15 +103,18 @@ export async function handleMergeWorktreeCommand({ command, appId, }) {
103
103
  await command.editReply('Rebase conflict detected. Asking the model to resolve...');
104
104
  await sendPromptToModel({
105
105
  prompt: [
106
- 'A rebase conflict occurred while merging this worktree into the default branch.',
106
+ `A rebase conflict occurred while merging this worktree into \`${result.target}\`.`,
107
107
  'Rebasing multiple commits can pause on each commit that conflicts, so you may need to repeat the resolve/continue loop several times.',
108
- 'Please resolve the rebase conflicts:',
109
- '1. Check `git status` to see which files have conflicts',
110
- '2. Edit the conflicted files to resolve the merge markers',
111
- '3. Stage resolved files with `git add`',
112
- '4. Continue the rebase with `git rebase --continue`',
113
- '5. If git reports more conflicts, repeat steps 1-4 until the rebase finishes (no more MERGE markers, `git status` shows no rebase in progress)',
114
- '6. Once the rebase is fully complete, tell me so I can run `/merge-worktree` again',
108
+ 'Before editing anything, first understand both sides so you preserve both intentions and do not drop features or fixes.',
109
+ '1. Check `git status` to see which files have conflicts and confirm the rebase is paused',
110
+ `2. Find the merge base between this worktree and \`${result.target}\`, then read the commit messages from both sides since that merge base so you understand the goal of each change`,
111
+ `3. Read the diffs from that merge base to both sides so you understand exactly what changed on this branch and on \`${result.target}\` before resolving conflicts`,
112
+ '4. Read the commit currently being replayed in the rebase so you know the intent of the specific conflicting patch',
113
+ '5. Edit the conflicted files to preserve both intended changes where possible instead of choosing one side wholesale',
114
+ '6. Stage resolved files with `git add`',
115
+ '7. Continue the rebase with `git rebase --continue`',
116
+ '8. If git reports more conflicts, repeat steps 1-7 until the rebase finishes (no more rebase in progress, `git status` is clean)',
117
+ '9. Once the rebase is fully complete, tell me so I can run `/merge-worktree` again',
115
118
  ].join('\n'),
116
119
  thread,
117
120
  projectDirectory: worktreeInfo.project_directory,
@@ -3132,8 +3132,8 @@ export class ThreadSessionRuntime {
3132
3132
  const truncate = (s, max) => {
3133
3133
  return s.length > max ? s.slice(0, max - 1) + '\u2026' : s;
3134
3134
  };
3135
- const truncatedFolder = truncate(folderName, 15);
3136
- const truncatedBranch = truncate(branchName, 15);
3135
+ const truncatedFolder = truncate(folderName, 30);
3136
+ const truncatedBranch = truncate(branchName, 30);
3137
3137
  const projectInfo = truncatedBranch
3138
3138
  ? `${truncatedFolder} ⋅ ${truncatedBranch} ⋅ `
3139
3139
  : `${truncatedFolder} ⋅ `;
@@ -368,10 +368,23 @@ Use --agent to specify which agent to use for the session:
368
368
  kimaki send --channel ${channelId} --prompt "Plan the refactor of the auth module" --agent plan${userArg}
369
369
  ${availableAgentsContext}
370
370
 
371
+ ## running opencode commands via kimaki send
372
+
373
+ You can trigger registered opencode commands (slash commands, skills, MCP prompts) by starting the \`--prompt\` with \`/commandname\`:
374
+
375
+ kimaki send --thread <thread_id> --prompt "/review fix the auth module"
376
+ kimaki send --channel ${channelId} --prompt "/build-cmd update dependencies"${userArg}
377
+
378
+ 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
+
371
380
  ## switching agents in the current session
372
381
 
373
382
  The user can switch the active agent mid-session using the Discord slash command \`/<agentname>-agent\`. For example if you are in plan mode and the user asks you to edit files, tell them to run \`/build-agent\` to switch to the build agent first.
374
383
 
384
+ You can also switch agents via \`kimaki send\`:
385
+
386
+ kimaki send --thread <thread_id> --prompt "/<agentname>-agent"
387
+
375
388
  ## scheduled sends and task management
376
389
 
377
390
  Use \`--send-at\` to schedule a one-time or recurring task:
@@ -135,10 +135,23 @@ describe('system-message', () => {
135
135
  - \`plan\`: planning only
136
136
  - \`build\`: edits files
137
137
 
138
+ ## running opencode commands via kimaki send
139
+
140
+ You can trigger registered opencode commands (slash commands, skills, MCP prompts) by starting the \`--prompt\` with \`/commandname\`:
141
+
142
+ kimaki send --thread <thread_id> --prompt "/review fix the auth module"
143
+ kimaki send --channel chan_123 --prompt "/build-cmd update dependencies" --user "Tommy"
144
+
145
+ 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\`).
146
+
138
147
  ## switching agents in the current session
139
148
 
140
149
  The user can switch the active agent mid-session using the Discord slash command \`/<agentname>-agent\`. For example if you are in plan mode and the user asks you to edit files, tell them to run \`/build-agent\` to switch to the build agent first.
141
150
 
151
+ You can also switch agents via \`kimaki send\`:
152
+
153
+ kimaki send --thread <thread_id> --prompt "/<agentname>-agent"
154
+
142
155
  ## scheduled sends and task management
143
156
 
144
157
  Use \`--send-at\` to schedule a one-time or recurring task:
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@otto-assistant/bridge",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.92",
5
+ "version": "0.4.93",
6
6
  "scripts": {
7
7
  "dev": "tsx src/bin.ts",
8
8
  "prepublishOnly": "pnpm generate && pnpm tsc",
@@ -39,15 +39,20 @@ Use this skill when scaffolding or fixing npm packages.
39
39
  - any runtime-required extra files (for example `schema.prisma`)
40
40
  - docs like `README.md` and `CHANGELOG.md`
41
41
  - if tests are inside src and gets included in dist, it's fine. don't try to exclude them
42
- 10. `scripts.build` should be `rimraf dist "*.tsbuildinfo" && tsc && chmod +x dist/cli.js` (skip the chmod
43
- if the package has no bin). No bundling. We remove dist to cleanup old transpiled files. Use `rimraf` here instead of bare shell globs so the script behaves the same in zsh, bash, and Windows shells even when no `.tsbuildinfo` file exists. This also removes the tsc incremental compilation state. Without that tsc would not generate again files to dist.
44
- Optionally include running scripts with tsx if needed to generate build artifacts.
45
- 11. `prepublishOnly` must always run `build` (optionally run generation before
46
- build when required). Always add this script:
42
+ 10. `scripts.build` should be `tsc && chmod +x dist/cli.js` (skip the chmod if
43
+ the package has no bin). No bundling. Do not delete `dist/` in `build` by
44
+ default because forcing a clean build on every local build can cause
45
+ issues. Optionally include running scripts with `tsx` if needed to
46
+ generate build artifacts.
47
+ 11. `prepublishOnly` must always do the cleanup before `build` (optionally run
48
+ generation before build when required). Always add this script:
47
49
  ```json
48
- { "prepublishOnly": "pnpm build" }
50
+ { "prepublishOnly": "rimraf dist \"*.tsbuildinfo\" && pnpm build" }
49
51
  ```
50
- This ensures `dist/` is fresh before every `npm publish`.
52
+ This ensures `dist/` is fresh before every `npm publish`, so deleted files
53
+ do not accidentally stay in the published package. Use `rimraf` here
54
+ instead of bare shell globs so the script behaves the same in zsh, bash,
55
+ and Windows shells even when no `.tsbuildinfo` file exists.
51
56
 
52
57
  ## bin field
53
58
 
@@ -71,8 +76,8 @@ Add the shebang as the first line of the source file (`src/cli.ts`):
71
76
  ```
72
77
 
73
78
  `tsc` preserves the shebang in the emitted `.js` file. The `chmod +x` is
74
- already part of the `build` script, so `prepublishOnly: "pnpm build"` handles
75
- it automatically.
79
+ already part of the `build` script, so `prepublishOnly` still gets it through
80
+ `pnpm build` after the cleanup step.
76
81
 
77
82
  ## Reading package version at runtime
78
83
 
@@ -950,7 +950,8 @@ describe('agent model resolution', () => {
950
950
  --- from: assistant (TestBot)
951
951
  ⬥ ok
952
952
  *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
953
- Switched to **plan** agent for this session next messages (was **test-agent**)
953
+ Switched to **plan** agent for this session (was **test-agent**)
954
+ The agent will change on the next message.
954
955
  --- from: user (agent-model-tester)
955
956
  Reply with exactly: after-switch-msg
956
957
  --- from: assistant (TestBot)
@@ -88,7 +88,13 @@ describe('rotateAnthropicAccount', () => {
88
88
  anthropic?: { refresh?: string }
89
89
  }
90
90
 
91
- expect(rotated).toMatchObject({ refresh: 'refresh-second' })
91
+ expect(rotated).toMatchObject({
92
+ auth: { refresh: 'refresh-second' },
93
+ fromLabel: '#1 (refresh-...irst)',
94
+ toLabel: '#2 (refresh-...cond)',
95
+ fromIndex: 0,
96
+ toIndex: 1,
97
+ })
92
98
  expect(store.activeIndex).toBe(1)
93
99
  expect(authJson.anthropic?.refresh).toBe('refresh-second')
94
100
  expect(authSetCalls).toEqual([
@@ -823,6 +823,13 @@ const AnthropicAuthPlugin: Plugin = async ({ client }) => {
823
823
  if (shouldRotateAuth(response.status, bodyText)) {
824
824
  const rotated = await rotateAnthropicAccount(freshAuth, client)
825
825
  if (rotated) {
826
+ // Show toast notification so Discord thread shows the rotation
827
+ client.tui.showToast({
828
+ body: {
829
+ message: `Switching from account ${rotated.fromLabel} to account ${rotated.toLabel}`,
830
+ variant: 'info',
831
+ },
832
+ }).catch(() => {})
826
833
  const retryAuth = await getFreshOAuth(getAuth, client)
827
834
  if (retryAuth) {
828
835
  response = await runRequest(retryAuth)
@@ -133,6 +133,21 @@ export async function saveAccountStore(store: AccountStore) {
133
133
  await writeJson(accountsFilePath(), normalizeAccountStore(store))
134
134
  }
135
135
 
136
+ /** Short label for an account: first 8 + last 4 chars of refresh token. */
137
+ export function accountLabel(account: OAuthStored, index?: number): string {
138
+ const r = account.refresh
139
+ const short = r.length > 12 ? `${r.slice(0, 8)}...${r.slice(-4)}` : r
140
+ return index !== undefined ? `#${index + 1} (${short})` : short
141
+ }
142
+
143
+ export type RotationResult = {
144
+ auth: OAuthStored
145
+ fromLabel: string
146
+ toLabel: string
147
+ fromIndex: number
148
+ toIndex: number
149
+ }
150
+
136
151
  function findCurrentAccountIndex(store: AccountStore, auth: OAuthStored) {
137
152
  if (!store.accounts.length) return 0
138
153
  const byRefresh = store.accounts.findIndex((account) => {
@@ -206,16 +221,21 @@ export async function setAnthropicAuth(
206
221
  export async function rotateAnthropicAccount(
207
222
  auth: OAuthStored,
208
223
  client: Parameters<Plugin>[0]['client'],
209
- ) {
224
+ ): Promise<RotationResult | undefined> {
210
225
  return withAuthStateLock(async () => {
211
226
  const store = await loadAccountStore()
212
227
  if (store.accounts.length < 2) return undefined
213
228
 
214
229
  const currentIndex = findCurrentAccountIndex(store, auth)
230
+ const currentAccount = store.accounts[currentIndex]
215
231
  const nextIndex = (currentIndex + 1) % store.accounts.length
216
232
  const nextAccount = store.accounts[nextIndex]
217
233
  if (!nextAccount) return undefined
218
234
 
235
+ const fromLabel = currentAccount
236
+ ? accountLabel(currentAccount, currentIndex)
237
+ : accountLabel(auth, currentIndex)
238
+
219
239
  nextAccount.lastUsed = Date.now()
220
240
  store.activeIndex = nextIndex
221
241
  await saveAccountStore(store)
@@ -227,7 +247,13 @@ export async function rotateAnthropicAccount(
227
247
  expires: nextAccount.expires,
228
248
  }
229
249
  await setAnthropicAuth(nextAuth, client)
230
- return nextAuth
250
+ return {
251
+ auth: nextAuth,
252
+ fromLabel,
253
+ toLabel: accountLabel(nextAccount, nextIndex),
254
+ fromIndex: currentIndex,
255
+ toIndex: nextIndex,
256
+ }
231
257
  })
232
258
  }
233
259
 
package/src/cli.ts CHANGED
@@ -125,6 +125,7 @@ import {
125
125
  type ScheduledTaskPayload,
126
126
  } from './task-schedule.js'
127
127
  import {
128
+ accountLabel,
128
129
  accountsFilePath,
129
130
  loadAccountStore,
130
131
  removeAccount,
@@ -3178,8 +3179,7 @@ cli
3178
3179
 
3179
3180
  store.accounts.forEach((account, index) => {
3180
3181
  const active = index === store.activeIndex ? '*' : ' '
3181
- const label = `${account.refresh.slice(0, 8)}...${account.refresh.slice(-4)}`
3182
- console.log(`${active} ${index + 1}. ${label}`)
3182
+ console.log(`${active} ${index + 1}. ${accountLabel(account)}`)
3183
3183
  })
3184
3184
 
3185
3185
  process.exit(0)
@@ -379,7 +379,7 @@ export async function handleAgentSelectMenu(
379
379
 
380
380
  if (context.isThread && context.sessionId) {
381
381
  await interaction.editReply({
382
- content: `Agent preference set for this session next messages: **${selectedAgent}**`,
382
+ content: `Agent preference set for this session: **${selectedAgent}**\nThe agent will change on the next message.`,
383
383
  components: [],
384
384
  })
385
385
  } else {
@@ -457,7 +457,7 @@ export async function handleQuickAgentCommand({
457
457
 
458
458
  if (context.isThread && context.sessionId) {
459
459
  await command.editReply({
460
- content: `Switched to **${resolvedAgentName}** agent for this session next messages${previousText}`,
460
+ content: `Switched to **${resolvedAgentName}** agent for this session${previousText}\nThe agent will change on the next message.`,
461
461
  })
462
462
  } else {
463
463
  await command.editReply({
@@ -155,15 +155,18 @@ export async function handleMergeWorktreeCommand({
155
155
  )
156
156
  await sendPromptToModel({
157
157
  prompt: [
158
- 'A rebase conflict occurred while merging this worktree into the default branch.',
158
+ `A rebase conflict occurred while merging this worktree into \`${result.target}\`.`,
159
159
  'Rebasing multiple commits can pause on each commit that conflicts, so you may need to repeat the resolve/continue loop several times.',
160
- 'Please resolve the rebase conflicts:',
161
- '1. Check `git status` to see which files have conflicts',
162
- '2. Edit the conflicted files to resolve the merge markers',
163
- '3. Stage resolved files with `git add`',
164
- '4. Continue the rebase with `git rebase --continue`',
165
- '5. If git reports more conflicts, repeat steps 1-4 until the rebase finishes (no more MERGE markers, `git status` shows no rebase in progress)',
166
- '6. Once the rebase is fully complete, tell me so I can run `/merge-worktree` again',
160
+ 'Before editing anything, first understand both sides so you preserve both intentions and do not drop features or fixes.',
161
+ '1. Check `git status` to see which files have conflicts and confirm the rebase is paused',
162
+ `2. Find the merge base between this worktree and \`${result.target}\`, then read the commit messages from both sides since that merge base so you understand the goal of each change`,
163
+ `3. Read the diffs from that merge base to both sides so you understand exactly what changed on this branch and on \`${result.target}\` before resolving conflicts`,
164
+ '4. Read the commit currently being replayed in the rebase so you know the intent of the specific conflicting patch',
165
+ '5. Edit the conflicted files to preserve both intended changes where possible instead of choosing one side wholesale',
166
+ '6. Stage resolved files with `git add`',
167
+ '7. Continue the rebase with `git rebase --continue`',
168
+ '8. If git reports more conflicts, repeat steps 1-7 until the rebase finishes (no more rebase in progress, `git status` is clean)',
169
+ '9. Once the rebase is fully complete, tell me so I can run `/merge-worktree` again',
167
170
  ].join('\n'),
168
171
  thread,
169
172
  projectDirectory: worktreeInfo.project_directory,
@@ -4111,8 +4111,8 @@ export class ThreadSessionRuntime {
4111
4111
  const truncate = (s: string, max: number) => {
4112
4112
  return s.length > max ? s.slice(0, max - 1) + '\u2026' : s
4113
4113
  }
4114
- const truncatedFolder = truncate(folderName, 15)
4115
- const truncatedBranch = truncate(branchName, 15)
4114
+ const truncatedFolder = truncate(folderName, 30)
4115
+ const truncatedBranch = truncate(branchName, 30)
4116
4116
  const projectInfo = truncatedBranch
4117
4117
  ? `${truncatedFolder} ⋅ ${truncatedBranch} ⋅ `
4118
4118
  : `${truncatedFolder} ⋅ `
@@ -142,10 +142,23 @@ describe('system-message', () => {
142
142
  - \`plan\`: planning only
143
143
  - \`build\`: edits files
144
144
 
145
+ ## running opencode commands via kimaki send
146
+
147
+ You can trigger registered opencode commands (slash commands, skills, MCP prompts) by starting the \`--prompt\` with \`/commandname\`:
148
+
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"
151
+
152
+ 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
+
145
154
  ## switching agents in the current session
146
155
 
147
156
  The user can switch the active agent mid-session using the Discord slash command \`/<agentname>-agent\`. For example if you are in plan mode and the user asks you to edit files, tell them to run \`/build-agent\` to switch to the build agent first.
148
157
 
158
+ You can also switch agents via \`kimaki send\`:
159
+
160
+ kimaki send --thread <thread_id> --prompt "/<agentname>-agent"
161
+
149
162
  ## scheduled sends and task management
150
163
 
151
164
  Use \`--send-at\` to schedule a one-time or recurring task:
@@ -477,10 +477,23 @@ Use --agent to specify which agent to use for the session:
477
477
  kimaki send --channel ${channelId} --prompt "Plan the refactor of the auth module" --agent plan${userArg}
478
478
  ${availableAgentsContext}
479
479
 
480
+ ## running opencode commands via kimaki send
481
+
482
+ You can trigger registered opencode commands (slash commands, skills, MCP prompts) by starting the \`--prompt\` with \`/commandname\`:
483
+
484
+ kimaki send --thread <thread_id> --prompt "/review fix the auth module"
485
+ kimaki send --channel ${channelId} --prompt "/build-cmd update dependencies"${userArg}
486
+
487
+ 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\`).
488
+
480
489
  ## switching agents in the current session
481
490
 
482
491
  The user can switch the active agent mid-session using the Discord slash command \`/<agentname>-agent\`. For example if you are in plan mode and the user asks you to edit files, tell them to run \`/build-agent\` to switch to the build agent first.
483
492
 
493
+ You can also switch agents via \`kimaki send\`:
494
+
495
+ kimaki send --thread <thread_id> --prompt "/<agentname>-agent"
496
+
484
497
  ## scheduled sends and task management
485
498
 
486
499
  Use \`--send-at\` to schedule a one-time or recurring task: