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