@otto-assistant/bridge 0.4.97 → 0.4.101

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 (39) hide show
  1. package/dist/agent-model.e2e.test.js +7 -1
  2. package/dist/anthropic-auth-plugin.js +227 -176
  3. package/dist/cli-send-thread.e2e.test.js +4 -7
  4. package/dist/cli.js +2 -2
  5. package/dist/commands/login.js +6 -4
  6. package/dist/commands/screenshare.js +1 -1
  7. package/dist/commands/screenshare.test.js +2 -2
  8. package/dist/commands/vscode.js +269 -0
  9. package/dist/context-awareness-plugin.js +8 -38
  10. package/dist/db.js +1 -0
  11. package/dist/discord-command-registration.js +5 -0
  12. package/dist/gateway-proxy-reconnect.e2e.test.js +2 -2
  13. package/dist/interaction-handler.js +4 -0
  14. package/dist/kimaki-opencode-plugin.js +3 -1
  15. package/dist/memory-overview-plugin.js +126 -0
  16. package/dist/system-message.js +23 -22
  17. package/dist/system-message.test.js +23 -22
  18. package/dist/system-prompt-drift-plugin.js +41 -11
  19. package/dist/utils.js +1 -1
  20. package/package.json +1 -1
  21. package/src/agent-model.e2e.test.ts +8 -1
  22. package/src/anthropic-auth-plugin.ts +574 -451
  23. package/src/cli-send-thread.e2e.test.ts +6 -7
  24. package/src/cli.ts +2 -2
  25. package/src/commands/login.ts +6 -4
  26. package/src/commands/screenshare.test.ts +2 -2
  27. package/src/commands/screenshare.ts +1 -1
  28. package/src/commands/vscode.ts +342 -0
  29. package/src/context-awareness-plugin.ts +11 -42
  30. package/src/db.ts +1 -0
  31. package/src/discord-command-registration.ts +7 -0
  32. package/src/gateway-proxy-reconnect.e2e.test.ts +2 -2
  33. package/src/interaction-handler.ts +5 -0
  34. package/src/kimaki-opencode-plugin.ts +3 -1
  35. package/src/memory-overview-plugin.ts +161 -0
  36. package/src/system-message.test.ts +23 -22
  37. package/src/system-message.ts +23 -22
  38. package/src/system-prompt-drift-plugin.ts +48 -12
  39. package/src/utils.ts +1 -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,
@@ -129,6 +129,8 @@ const PROVIDER_POPULARITY_ORDER: string[] = [
129
129
  'xai',
130
130
  'groq',
131
131
  'deepseek',
132
+ 'opencode',
133
+ 'opencode-go',
132
134
  'mistral',
133
135
  'openrouter',
134
136
  'fireworks-ai',
@@ -137,12 +139,12 @@ const PROVIDER_POPULARITY_ORDER: string[] = [
137
139
  'azure',
138
140
  'google-vertex',
139
141
  'google-vertex-anthropic',
140
- 'cohere',
142
+ // 'cohere',
141
143
  'cerebras',
142
- 'perplexity',
144
+ // 'perplexity',
143
145
  'cloudflare-workers-ai',
144
- 'novita-ai',
145
- 'huggingface',
146
+ // 'novita-ai',
147
+ // 'huggingface',
146
148
  'deepinfra',
147
149
  'github-models',
148
150
  'lmstudio',
@@ -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
@@ -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)
@@ -1,7 +1,6 @@
1
1
  // OpenCode plugin that injects synthetic message parts for context awareness:
2
2
  // - Git branch / detached HEAD changes
3
3
  // - Working directory (pwd) changes (e.g. after /new-worktree mid-session)
4
- // - MEMORY.md table of contents on first message
5
4
  // - MEMORY.md reminder after a large assistant reply
6
5
  // - Onboarding tutorial instructions (when TUTORIAL_WELCOME_TEXT detected)
7
6
  //
@@ -18,8 +17,6 @@
18
17
 
19
18
  import type { Plugin } from '@opencode-ai/plugin'
20
19
  import crypto from 'node:crypto'
21
- import fs from 'node:fs'
22
- import path from 'node:path'
23
20
  import * as errore from 'errore'
24
21
  import {
25
22
  createPluginLogger,
@@ -29,7 +26,6 @@ import {
29
26
  import { setDataDir } from './config.js'
30
27
  import { initSentry, notifyError } from './sentry.js'
31
28
  import { execAsync } from './exec-async.js'
32
- import { condenseMemoryMd } from './condense-memory.js'
33
29
  import {
34
30
  ONBOARDING_TUTORIAL_INSTRUCTIONS,
35
31
  TUTORIAL_WELCOME_TEXT,
@@ -49,7 +45,6 @@ type GitState = {
49
45
  // All per-session mutable state in one place. One Map entry, one delete.
50
46
  type SessionState = {
51
47
  gitState: GitState | undefined
52
- memoryInjected: boolean
53
48
  lastMemoryReminderAssistantMessageId: string | undefined
54
49
  tutorialInjected: boolean
55
50
  // Last directory observed via session.get(). Refreshed on each real user
@@ -60,17 +55,6 @@ type SessionState = {
60
55
  announcedDirectory: string | undefined
61
56
  }
62
57
 
63
- function createSessionState(): SessionState {
64
- return {
65
- gitState: undefined,
66
- memoryInjected: false,
67
- lastMemoryReminderAssistantMessageId: undefined,
68
- tutorialInjected: false,
69
- resolvedDirectory: undefined,
70
- announcedDirectory: undefined,
71
- }
72
- }
73
-
74
58
  // Minimal type for the opencode plugin client (v1 SDK style with path objects).
75
59
  type PluginClient = {
76
60
  session: {
@@ -147,10 +131,6 @@ type AssistantMessageInfo = {
147
131
  tokens?: AssistantTokenUsage
148
132
  }
149
133
 
150
- function getOutputTokenTotal(tokens: AssistantTokenUsage): number {
151
- return Math.max(0, tokens.output + tokens.reasoning)
152
- }
153
-
154
134
  export function shouldInjectMemoryReminderFromLatestAssistant({
155
135
  lastMemoryReminderAssistantMessageId,
156
136
  latestAssistantMessage,
@@ -175,7 +155,10 @@ export function shouldInjectMemoryReminderFromLatestAssistant({
175
155
  if (lastMemoryReminderAssistantMessageId === latestAssistantMessage.id) {
176
156
  return { inject: false }
177
157
  }
178
- const outputTokens = getOutputTokenTotal(latestAssistantMessage.tokens)
158
+ const outputTokens = Math.max(
159
+ 0,
160
+ latestAssistantMessage.tokens.output + latestAssistantMessage.tokens.reasoning,
161
+ )
179
162
  if (outputTokens < threshold) {
180
163
  return { inject: false }
181
164
  }
@@ -311,7 +294,13 @@ const contextAwarenessPlugin: Plugin = async ({ directory, client }) => {
311
294
  if (existing) {
312
295
  return existing
313
296
  }
314
- const state = createSessionState()
297
+ const state: SessionState = {
298
+ gitState: undefined,
299
+ lastMemoryReminderAssistantMessageId: undefined,
300
+ tutorialInjected: false,
301
+ resolvedDirectory: undefined,
302
+ announcedDirectory: undefined,
303
+ }
315
304
  sessions.set(sessionID, state)
316
305
  return state
317
306
  }
@@ -412,26 +401,6 @@ const contextAwarenessPlugin: Plugin = async ({ directory, client }) => {
412
401
  })
413
402
  }
414
403
 
415
- // -- MEMORY.md injection --
416
- if (!state.memoryInjected) {
417
- state.memoryInjected = true
418
- const memoryPath = path.join(effectiveDirectory, 'MEMORY.md')
419
- const memoryContent = await fs.promises
420
- .readFile(memoryPath, 'utf-8')
421
- .catch(() => null)
422
- if (memoryContent) {
423
- const condensed = condenseMemoryMd(memoryContent)
424
- output.parts.push({
425
- id: `prt_${crypto.randomUUID()}`,
426
- sessionID,
427
- messageID,
428
- type: 'text' as const,
429
- text: `<system-reminder>Project memory from MEMORY.md (condensed table of contents, line numbers shown):\n${condensed}\nOnly headings are shown above — section bodies are hidden. Use Grep to search MEMORY.md for specific topics, or Read with offset and limit to read a section's content. When writing to MEMORY.md, keep titles concise (under 10 words) and content brief (2-3 sentences max). Only track non-obvious learnings that prevent future mistakes and are not already documented in code comments or AGENTS.md. Do not duplicate information that is self-evident from the code.</system-reminder>`,
430
- synthetic: true,
431
- })
432
- }
433
- }
434
-
435
404
  const memoryReminder = shouldInjectMemoryReminderFromLatestAssistant({
436
405
  lastMemoryReminderAssistantMessageId:
437
406
  state.lastMemoryReminderAssistantMessageId,
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)
@@ -5,12 +5,14 @@
5
5
  //
6
6
  // Plugins are split into focused modules:
7
7
  // - ipc-tools-plugin: file upload + action buttons (IPC-based Discord tools)
8
- // - context-awareness-plugin: branch, pwd, memory, time gap, onboarding tutorial
8
+ // - context-awareness-plugin: branch, pwd, memory reminder, onboarding tutorial
9
+ // - memory-overview-plugin: frozen MEMORY.md heading overview per session
9
10
  // - opencode-interrupt-plugin: interrupt queued messages at step boundaries
10
11
  // - kitty-graphics-plugin: extract Kitty Graphics Protocol images from bash output
11
12
 
12
13
  export { ipcToolsPlugin } from './ipc-tools-plugin.js'
13
14
  export { contextAwarenessPlugin } from './context-awareness-plugin.js'
15
+ export { memoryOverviewPlugin } from './memory-overview-plugin.js'
14
16
  export { interruptOpencodeSessionOnUserMessage } from './opencode-interrupt-plugin.js'
15
17
  export { systemPromptDriftPlugin } from './system-prompt-drift-plugin.js'
16
18
  export { anthropicAuthPlugin } from './anthropic-auth-plugin.js'