@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.
@@ -16,7 +16,7 @@ describe('system-message', () => {
16
16
  ],
17
17
  }).replace(/`[^`]*\/kimaki\.log`/, '`<data-dir>/kimaki.log`')).toMatchInlineSnapshot(`
18
18
  "
19
- The user is reading your messages from inside Discord, via kimaki.xyz
19
+ The user is reading your messages from inside Discord, via kimaki.dev
20
20
 
21
21
  ## bash tool
22
22
 
@@ -85,39 +85,40 @@ describe('system-message', () => {
85
85
 
86
86
  To start a new thread/session in this channel pro-grammatically, run:
87
87
 
88
- kimaki send --channel chan_123 --prompt "your prompt here" --user "Tommy"
88
+ kimaki send --channel chan_123 --prompt "your prompt here" --agent <current_agent> --user "Tommy"
89
89
 
90
90
  You can use this to "spawn" parallel helper sessions like teammates: start new threads with focused prompts, then come back and collect the results.
91
+ 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.
91
92
 
92
93
  IMPORTANT: NEVER use \`--worktree\` unless the user explicitly asks for a worktree. Default to creating normal threads without worktrees.
93
94
 
94
95
  To send a prompt to an existing thread instead of creating a new one:
95
96
 
96
- kimaki send --thread <thread_id> --prompt "follow-up prompt"
97
+ kimaki send --thread <thread_id> --prompt "follow-up prompt" --agent <current_agent>
97
98
 
98
99
  Use this when you already have the Discord thread ID.
99
100
 
100
101
  To send to the thread associated with a known session:
101
102
 
102
- kimaki send --session <session_id> --prompt "follow-up prompt"
103
+ kimaki send --session <session_id> --prompt "follow-up prompt" --agent <current_agent>
103
104
 
104
105
  Use this when you have the OpenCode session ID.
105
106
 
106
107
  Use --notify-only to create a notification thread without starting an AI session:
107
108
 
108
- kimaki send --channel chan_123 --prompt "User cancelled subscription" --notify-only --user "Tommy"
109
+ kimaki send --channel chan_123 --prompt "User cancelled subscription" --notify-only --agent <current_agent> --user "Tommy"
109
110
 
110
111
  Use --user to add a specific Discord user to the new thread:
111
112
 
112
- kimaki send --channel chan_123 --prompt "Review the latest CI failure" --user "Tommy"
113
+ kimaki send --channel chan_123 --prompt "Review the latest CI failure" --agent <current_agent> --user "Tommy"
113
114
 
114
115
  Use --worktree to create a git worktree for the session (ONLY when the user explicitly asks for a worktree):
115
116
 
116
- kimaki send --channel chan_123 --prompt "Add dark mode support" --worktree dark-mode --user "Tommy"
117
+ kimaki send --channel chan_123 --prompt "Add dark mode support" --worktree dark-mode --agent <current_agent> --user "Tommy"
117
118
 
118
119
  Use --cwd to start a session in an existing git worktree directory (must be a worktree of the project):
119
120
 
120
- kimaki send --channel chan_123 --prompt "Continue work on feature" --cwd /path/to/existing-worktree --user "Tommy"
121
+ kimaki send --channel chan_123 --prompt "Continue work on feature" --cwd /path/to/existing-worktree --agent <current_agent> --user "Tommy"
121
122
 
122
123
  Important:
123
124
  - NEVER use \`--worktree\` unless the user explicitly requests a worktree. Most tasks should use normal threads without worktrees.
@@ -139,8 +140,8 @@ describe('system-message', () => {
139
140
 
140
141
  You can trigger registered opencode commands (slash commands, skills, MCP prompts) by starting the \`--prompt\` with \`/commandname\`:
141
142
 
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"
143
+ kimaki send --thread <thread_id> --prompt "/review fix the auth module" --agent <current_agent>
144
+ kimaki send --channel chan_123 --prompt "/build-cmd update dependencies" --agent <current_agent> --user "Tommy"
144
145
 
145
146
  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
147
 
@@ -150,14 +151,14 @@ describe('system-message', () => {
150
151
 
151
152
  You can also switch agents via \`kimaki send\`:
152
153
 
153
- kimaki send --thread <thread_id> --prompt "/<agentname>-agent"
154
+ kimaki send --thread <thread_id> --prompt "/<agentname>-agent" --agent <current_agent>
154
155
 
155
156
  ## scheduled sends and task management
156
157
 
157
158
  Use \`--send-at\` to schedule a one-time or recurring task:
158
159
 
159
- kimaki send --channel chan_123 --prompt "Reminder: review open PRs" --send-at "2026-03-01T09:00:00Z" --user "Tommy"
160
- kimaki send --channel chan_123 --prompt "Run weekly test suite and summarize failures" --send-at "0 9 * * 1" --user "Tommy"
160
+ kimaki send --channel chan_123 --prompt "Reminder: review open PRs" --send-at "2026-03-01T09:00:00Z" --agent <current_agent> --user "Tommy"
161
+ kimaki send --channel chan_123 --prompt "Run weekly test suite and summarize failures" --send-at "0 9 * * 1" --agent <current_agent> --user "Tommy"
161
162
 
162
163
  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).
163
164
  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.
@@ -191,13 +192,13 @@ describe('system-message', () => {
191
192
 
192
193
  Use case patterns:
193
194
  - Reminder flows: create deadline reminders in this channel with one-time \`--send-at\`; mention only if action is required.
194
- - 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.
195
+ - 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.
195
196
  - Weekly QA: schedule "run full test suite, inspect failures, post summary, and mention @username only when failures require review".
196
197
  - Weekly benchmark automation: schedule a benchmark prompt that runs model evals, writes JSON outputs in the repo, commits results, and mentions only for regressions.
197
198
  - 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.
198
199
  - 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:
199
200
 
200
- kimaki send --session ses_123 --prompt "Reminder: <@USER_ID> you asked to be reminded about this thread." --send-at "<future_UTC_time>" --notify-only
201
+ 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>
201
202
 
202
203
  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.
203
204
 
@@ -212,7 +213,7 @@ describe('system-message', () => {
212
213
  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:
213
214
 
214
215
  \`\`\`bash
215
- kimaki send --channel chan_123 --prompt "your task description" --worktree worktree-name --user "Tommy"
216
+ kimaki send --channel chan_123 --prompt "your task description" --worktree worktree-name --agent <current_agent> --user "Tommy"
216
217
  \`\`\`
217
218
 
218
219
  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.
@@ -228,7 +229,7 @@ describe('system-message', () => {
228
229
  Use \`--cwd\` to start a session in an existing git worktree directory instead of creating a new one:
229
230
 
230
231
  \`\`\`bash
231
- kimaki send --channel chan_123 --prompt "Continue work on feature X" --cwd /path/to/existing-worktree --user "Tommy"
232
+ kimaki send --channel chan_123 --prompt "Continue work on feature X" --cwd /path/to/existing-worktree --agent <current_agent> --user "Tommy"
232
233
  \`\`\`
233
234
 
234
235
  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.
@@ -242,7 +243,7 @@ describe('system-message', () => {
242
243
  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:
243
244
 
244
245
  \`\`\`bash
245
- kimaki send --channel chan_123 --prompt "Continuing from previous session: <summary of current task and state>" --user "Tommy"
246
+ kimaki send --channel chan_123 --prompt "Continuing from previous session: <summary of current task and state>" --agent <current_agent> --user "Tommy"
246
247
  \`\`\`
247
248
 
248
249
  The command automatically handles long prompts (over 2000 chars) by sending them as file attachments.
@@ -300,10 +301,10 @@ describe('system-message', () => {
300
301
 
301
302
  \`\`\`bash
302
303
  # Send to a specific channel
303
- kimaki send --channel <channel_id> --prompt "Plan how to update the API client to v2"
304
+ kimaki send --channel <channel_id> --prompt "Plan how to update the API client to v2" --agent <current_agent>
304
305
 
305
306
  # Or use --project to resolve from directory
306
- kimaki send --project /path/to/other-repo --prompt "Plan how to bump version to 1.2.0"
307
+ kimaki send --project /path/to/other-repo --prompt "Plan how to bump version to 1.2.0" --agent <current_agent>
307
308
  \`\`\`
308
309
 
309
310
  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.
@@ -326,10 +327,10 @@ describe('system-message', () => {
326
327
 
327
328
  \`\`\`bash
328
329
  # Start a session and wait for it to finish
329
- kimaki send --channel <channel_id> --prompt "Fix the auth bug" --wait
330
+ kimaki send --channel <channel_id> --prompt "Fix the auth bug" --wait --agent <current_agent>
330
331
 
331
332
  # Send to an existing thread and wait
332
- kimaki send --thread <thread_id> --prompt "Run the tests" --wait
333
+ kimaki send --thread <thread_id> --prompt "Run the tests" --wait --agent <current_agent>
333
334
  \`\`\`
334
335
 
335
336
  The command exits with the session markdown on stdout once the model finishes responding.
@@ -70,7 +70,6 @@ function writeSystemPromptDiffFile({ dataDir, sessionId, beforePrompt, afterProm
70
70
  const timestamp = new Date().toISOString().replaceAll(':', '-');
71
71
  const sessionDir = path.join(getSystemPromptDiffDir({ dataDir }), sessionId);
72
72
  const filePath = path.join(sessionDir, `${timestamp}.diff`);
73
- const latestPromptPath = path.join(sessionDir, `${timestamp}.md`);
74
73
  const fileContent = [
75
74
  `Session: ${sessionId}`,
76
75
  `Created: ${new Date().toISOString()}`,
@@ -88,7 +87,6 @@ function writeSystemPromptDiffFile({ dataDir, sessionId, beforePrompt, afterProm
88
87
  additions: diff.additions,
89
88
  deletions: diff.deletions,
90
89
  filePath,
91
- latestPromptPath,
92
90
  };
93
91
  },
94
92
  catch: (error) => {
@@ -109,6 +107,7 @@ function getOrCreateSessionState({ sessions, sessionId, }) {
109
107
  comparedTurn: 0,
110
108
  previousTurnContext: undefined,
111
109
  currentTurnContext: undefined,
110
+ pendingCompareTimeout: undefined,
112
111
  };
113
112
  sessions.set(sessionId, state);
114
113
  return state;
@@ -162,8 +161,7 @@ async function handleSystemTransform({ input, output, sessions, dataDir, client,
162
161
  message: appendToastSessionMarker({
163
162
  sessionId,
164
163
  message: `system prompt changed since the previous message (+${diffFileResult.additions} / -${diffFileResult.deletions}). ` +
165
- `Diff: \`${abbreviatePath(diffFileResult.filePath)}\`. ` +
166
- `Latest prompt: \`${abbreviatePath(diffFileResult.latestPromptPath)}\``,
164
+ `Diff: \`${abbreviatePath(diffFileResult.filePath)}\`. `
167
165
  }),
168
166
  },
169
167
  });
@@ -193,13 +191,41 @@ const systemPromptDriftPlugin = async ({ client, directory }) => {
193
191
  'experimental.chat.system.transform': async (input, output) => {
194
192
  const result = await errore.tryAsync({
195
193
  try: async () => {
196
- await handleSystemTransform({
197
- input,
198
- output,
199
- sessions,
200
- dataDir,
201
- client,
202
- });
194
+ const sessionId = input.sessionID;
195
+ if (!sessionId) {
196
+ return;
197
+ }
198
+ const state = getOrCreateSessionState({ sessions, sessionId });
199
+ if (state.pendingCompareTimeout) {
200
+ clearTimeout(state.pendingCompareTimeout);
201
+ }
202
+ // Delay one tick so other system-transform hooks can finish mutating
203
+ // output.system before we snapshot it for drift detection.
204
+ state.pendingCompareTimeout = setTimeout(() => {
205
+ state.pendingCompareTimeout = undefined;
206
+ void errore.tryAsync({
207
+ try: async () => {
208
+ await handleSystemTransform({
209
+ input,
210
+ output,
211
+ sessions,
212
+ dataDir,
213
+ client,
214
+ });
215
+ },
216
+ catch: (error) => {
217
+ return new Error('system prompt drift transform hook failed', {
218
+ cause: error,
219
+ });
220
+ },
221
+ }).then((delayedResult) => {
222
+ if (!(delayedResult instanceof Error)) {
223
+ return;
224
+ }
225
+ logger.warn(`[system-prompt-drift-plugin] ${formatPluginErrorWithStack(delayedResult)}`);
226
+ void notifyError(delayedResult, 'system prompt drift plugin transform hook failed');
227
+ });
228
+ }, 0);
203
229
  },
204
230
  catch: (error) => {
205
231
  return new Error('system prompt drift transform hook failed', {
@@ -222,6 +248,10 @@ const systemPromptDriftPlugin = async ({ client, directory }) => {
222
248
  if (!deletedSessionId) {
223
249
  return;
224
250
  }
251
+ const state = sessions.get(deletedSessionId);
252
+ if (state?.pendingCompareTimeout) {
253
+ clearTimeout(state.pendingCompareTimeout);
254
+ }
225
255
  sessions.delete(deletedSessionId);
226
256
  },
227
257
  catch: (error) => {
package/dist/utils.js CHANGED
@@ -50,7 +50,7 @@ export function generateBotInstallUrl({ clientId, permissions = [
50
50
  return url.toString();
51
51
  }
52
52
  export const KIMAKI_GATEWAY_APP_ID = process.env.KIMAKI_GATEWAY_APP_ID || '1477605701202481173';
53
- export const KIMAKI_WEBSITE_URL = process.env.KIMAKI_WEBSITE_URL || 'https://kimaki.xyz';
53
+ export const KIMAKI_WEBSITE_URL = process.env.KIMAKI_WEBSITE_URL || 'https://kimaki.dev';
54
54
  export function generateDiscordInstallUrlForBot({ appId, mode, clientId, clientSecret, gatewayCallbackUrl, reachableUrl, }) {
55
55
  if (mode !== 'gateway') {
56
56
  return generateBotInstallUrl({ clientId: appId });
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.97",
5
+ "version": "0.4.100",
6
6
  "scripts": {
7
7
  "dev": "tsx src/bin.ts",
8
8
  "prepublishOnly": "pnpm generate && pnpm tsc",
@@ -542,7 +542,14 @@ describe('agent model resolution', () => {
542
542
  afterAuthorId: discord.botUserId,
543
543
  })
544
544
 
545
- expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
545
+ const threadText = (await discord.thread(thread.id).text())
546
+ .split('\n')
547
+ .filter((line) => {
548
+ return !line.startsWith('⬦ info: Context cache discarded:')
549
+ })
550
+ .join('\n')
551
+
552
+ expect(threadText).toMatchInlineSnapshot(`
546
553
  "--- from: user (agent-model-tester)
547
554
  first message in thread
548
555
  Reply with exactly: reply-context-check
@@ -559,6 +559,8 @@ function toClaudeCodeToolName(name: string) {
559
559
  function sanitizeSystemText(text: string, onError?: (msg: string) => void) {
560
560
  const startIdx = text.indexOf(OPENCODE_IDENTITY)
561
561
  if (startIdx === -1) return text
562
+ // to find the last heading to match readhttps://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/prompt/anthropic.txt
563
+ // it contains the opencode injected prompt. you must keep the codeRefsMarker updated with that package
562
564
  const codeRefsMarker = '# Code References'
563
565
  const endIdx = text.indexOf(codeRefsMarker, startIdx)
564
566
  if (endIdx === -1) {
@@ -339,14 +339,13 @@ describe('kimaki send --channel thread creation', () => {
339
339
  })
340
340
 
341
341
  const allContent = botReplies.map((m) => {
342
- return m.content.slice(0, 200)
342
+ return m.content
343
343
  })
344
- expect(allContent).toMatchInlineSnapshot(`
345
- [
346
- "✗ opencode session error: Command not found: "hello-test". Available commands: init, review, goke, security-review, jitter, proxyman, gitchamber, event-sourcing-state, usecomputer, spiceflow, batch, x",
347
- "✗ OpenCode API error: Command not found: "hello-test". Available commands: init, review, goke, security-review, jitter, proxyman, gitchamber, event-sourcing-state, usecomputer, spiceflow, batch, x-art",
348
- ]
349
- `)
344
+ expect(
345
+ allContent.some((content) => {
346
+ return content.includes('Command not found: "hello-test"')
347
+ }),
348
+ ).toBe(true)
350
349
  } finally {
351
350
  store.setState({ registeredUserCommands: prevCommands })
352
351
  }
package/src/cli.ts CHANGED
@@ -141,7 +141,7 @@ const cliLogger = createLogger(LogPrefix.CLI)
141
141
  // These are hardcoded because they're deploy-time constants for the gateway infrastructure.
142
142
  const KIMAKI_GATEWAY_PROXY_URL =
143
143
  process.env.KIMAKI_GATEWAY_PROXY_URL ||
144
- 'wss://discord-gateway.kimaki.xyz'
144
+ 'wss://discord-gateway.kimaki.dev'
145
145
 
146
146
  const KIMAKI_GATEWAY_PROXY_REST_BASE_URL = getGatewayProxyRestBaseUrl({
147
147
  gatewayUrl: KIMAKI_GATEWAY_PROXY_URL,
@@ -3792,7 +3792,7 @@ cli
3792
3792
  port,
3793
3793
  tunnelId: options.tunnelId,
3794
3794
  localHost: options.host,
3795
- baseDomain: 'kimaki.xyz',
3795
+ baseDomain: 'kimaki.dev',
3796
3796
  serverUrl: options.server,
3797
3797
  command: command.length > 0 ? command : undefined,
3798
3798
  kill: options.kill,
@@ -17,12 +17,12 @@ describe('screenshare security defaults', () => {
17
17
 
18
18
  test('builds a secure noVNC URL', () => {
19
19
  const url = new URL(
20
- buildNoVncUrl({ tunnelHost: '0123456789abcdef-tunnel.kimaki.xyz' }),
20
+ buildNoVncUrl({ tunnelHost: '0123456789abcdef-tunnel.kimaki.dev' }),
21
21
  )
22
22
 
23
23
  expect(url.origin).toBe('https://novnc.com')
24
24
  expect(url.searchParams.get('host')).toBe(
25
- '0123456789abcdef-tunnel.kimaki.xyz',
25
+ '0123456789abcdef-tunnel.kimaki.dev',
26
26
  )
27
27
  expect(url.searchParams.get('port')).toBe('443')
28
28
  expect(url.searchParams.get('encrypt')).toBe('1')
@@ -40,7 +40,7 @@ const activeSessions = new Map<string, ScreenshareSession>()
40
40
  const VNC_PORT = 5900
41
41
  const MAX_SESSION_MINUTES = 30
42
42
  const MAX_SESSION_MS = MAX_SESSION_MINUTES * 60 * 1000
43
- const TUNNEL_BASE_DOMAIN = 'kimaki.xyz'
43
+ const TUNNEL_BASE_DOMAIN = 'kimaki.dev'
44
44
  const SCREENSHARE_TUNNEL_ID_BYTES = 16
45
45
 
46
46
  // Public noVNC client — we point it at our tunnel URL