kimaki 0.25.0 → 0.26.0
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 +135 -3
- package/dist/analytics.test.js +30 -1
- package/dist/cli-commands/send.js +13 -3
- package/dist/cli-commands/task.js +26 -7
- package/dist/cli-runner.js +66 -70
- package/dist/cli-runner.test.js +10 -1
- package/dist/commands/agent.js +87 -74
- package/dist/commands/model.js +18 -0
- package/dist/commands/multioauth.js +169 -1
- package/dist/commands/tasks.js +6 -0
- package/dist/commands/unset-model.js +5 -20
- package/dist/database.js +65 -0
- package/dist/discord-bot.js +7 -1
- package/dist/ipc-tools-plugin.js +4 -3
- package/dist/kimaki-opencode-plugin.js +2 -0
- package/dist/message-formatting.js +84 -5
- package/dist/message-formatting.test.js +160 -2
- package/dist/oauth-rotation-shared.js +7 -1
- package/dist/openai-auth-state.js +20 -0
- package/dist/schema.js +19 -0
- package/dist/session-handler/event-stream-state.js +147 -0
- package/dist/session-handler/event-stream-state.test.js +437 -1
- package/dist/session-handler/thread-session-runtime.js +90 -37
- package/dist/subagent-rate-limit-plugin.js +13 -2
- package/dist/system-message.js +18 -4
- package/dist/system-message.test.js +28 -4
- package/dist/task-runner.js +143 -12
- package/dist/task-schedule.js +12 -0
- package/dist/task-schedule.test.js +149 -2
- package/dist/xai-auth-plugin.js +237 -0
- package/dist/xai-auth-state.js +233 -0
- package/dist/xai-auth-state.test.js +183 -0
- package/package.json +6 -6
- package/skills/profano/SKILL.md +5 -3
- package/skills/spiceflow/SKILL.md +24 -1
- package/src/agent-model.e2e.test.ts +172 -4
- package/src/analytics.test.ts +30 -1
- package/src/analytics.ts +1 -0
- package/src/cli-commands/send.ts +22 -3
- package/src/cli-commands/task.ts +32 -19
- package/src/cli-runner.test.ts +16 -1
- package/src/cli-runner.ts +88 -89
- package/src/commands/agent.ts +120 -84
- package/src/commands/model.ts +25 -0
- package/src/commands/multioauth.ts +195 -2
- package/src/commands/tasks.ts +6 -0
- package/src/commands/unset-model.ts +5 -21
- package/src/database.ts +124 -0
- package/src/discord-bot.ts +7 -1
- package/src/ipc-tools-plugin.ts +4 -3
- package/src/kimaki-opencode-plugin.ts +2 -0
- package/src/message-formatting.test.ts +197 -2
- package/src/message-formatting.ts +98 -5
- package/src/oauth-rotation-shared.ts +6 -1
- package/src/openai-auth-state.ts +19 -0
- package/src/schema.sql +15 -0
- package/src/schema.ts +20 -0
- package/src/session-handler/event-stream-state.test.ts +490 -0
- package/src/session-handler/event-stream-state.ts +210 -0
- package/src/session-handler/model-utils.ts +1 -0
- package/src/session-handler/thread-session-runtime.ts +105 -44
- package/src/subagent-rate-limit-plugin.ts +12 -2
- package/src/system-message.test.ts +29 -4
- package/src/system-message.ts +20 -4
- package/src/task-runner.ts +168 -11
- package/src/task-schedule.test.ts +172 -2
- package/src/task-schedule.ts +23 -2
- package/src/xai-auth-plugin.ts +302 -0
- package/src/xai-auth-state.test.ts +224 -0
- package/src/xai-auth-state.ts +293 -0
package/src/cli-runner.ts
CHANGED
|
@@ -57,7 +57,6 @@ import { discordApiUrl, getDiscordRestApiUrl, getGatewayProxyRestBaseUrl, getInt
|
|
|
57
57
|
import crypto from 'node:crypto'
|
|
58
58
|
import path from 'node:path'
|
|
59
59
|
import fs from 'node:fs'
|
|
60
|
-
import os from 'node:os'
|
|
61
60
|
import { spawn } from 'node:child_process'
|
|
62
61
|
import { createLogger, LogPrefix } from './logger.js'
|
|
63
62
|
import { notifyError } from './sentry.js'
|
|
@@ -206,6 +205,44 @@ export async function resolveBotCredentials({ appIdOverride }: { appIdOverride?:
|
|
|
206
205
|
|
|
207
206
|
export { isThreadChannelType }
|
|
208
207
|
|
|
208
|
+
/** Wrap long lines so prompt.md is readable in Discord's attachment preview. */
|
|
209
|
+
function wrapPromptAttachmentText(prompt: string): string {
|
|
210
|
+
return prompt
|
|
211
|
+
.split('\n')
|
|
212
|
+
.flatMap((line) => {
|
|
213
|
+
if (line.length <= 120) {
|
|
214
|
+
return [line]
|
|
215
|
+
}
|
|
216
|
+
const wrapped: string[] = []
|
|
217
|
+
let remaining = line
|
|
218
|
+
const maxCol = 120
|
|
219
|
+
// Only soft-break at a space if it's reasonably close to maxCol,
|
|
220
|
+
// otherwise hard-break to avoid tiny fragments from early spaces
|
|
221
|
+
const minSoftBreak = 90
|
|
222
|
+
while (remaining.length > maxCol) {
|
|
223
|
+
const lastSpace = remaining.lastIndexOf(' ', maxCol)
|
|
224
|
+
const useSoftBreak = lastSpace >= minSoftBreak
|
|
225
|
+
const breakAt = useSoftBreak ? lastSpace : maxCol
|
|
226
|
+
wrapped.push(remaining.slice(0, breakAt))
|
|
227
|
+
// Only consume the separator space on soft breaks
|
|
228
|
+
remaining = useSoftBreak
|
|
229
|
+
? remaining.slice(breakAt + 1)
|
|
230
|
+
: remaining.slice(breakAt)
|
|
231
|
+
}
|
|
232
|
+
if (remaining.length > 0) {
|
|
233
|
+
wrapped.push(remaining)
|
|
234
|
+
}
|
|
235
|
+
return wrapped
|
|
236
|
+
})
|
|
237
|
+
.join('\n')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function promptAttachmentBlob(text: string) {
|
|
241
|
+
return new Blob([new Uint8Array(Buffer.from(text, 'utf8'))], {
|
|
242
|
+
type: 'text/markdown',
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
209
246
|
export async function sendDiscordMessageWithOptionalAttachment({
|
|
210
247
|
channelId,
|
|
211
248
|
prompt,
|
|
@@ -219,7 +256,8 @@ export async function sendDiscordMessageWithOptionalAttachment({
|
|
|
219
256
|
prompt: string
|
|
220
257
|
botToken: string
|
|
221
258
|
embeds?: Array<{ color: number; footer: { text: string } }>
|
|
222
|
-
|
|
259
|
+
/** Only `post` is used (short prompt / split paths). Accept a narrow shape so tests need no cast. */
|
|
260
|
+
rest: Pick<REST, 'post'>
|
|
223
261
|
/** When true, long messages are split into multiple Discord messages instead of
|
|
224
262
|
* being attached as a file. Useful for notify-only messages where the content
|
|
225
263
|
* should be directly visible in the channel. */
|
|
@@ -246,24 +284,26 @@ export async function sendDiscordMessageWithOptionalAttachment({
|
|
|
246
284
|
}
|
|
247
285
|
|
|
248
286
|
// When prompt exceeds Discord's limit, attach it as prompt.md alongside
|
|
249
|
-
// user files so nothing is silently lost.
|
|
287
|
+
// user files so nothing is silently lost. Build prompt.md from memory so
|
|
288
|
+
// parallel kimaki send processes never share or unlink a temp path.
|
|
250
289
|
const isLongPrompt = prompt.length > discordMaxLength
|
|
251
290
|
const content = isLongPrompt
|
|
252
291
|
? `Prompt attached as file (${prompt.length} chars)\n\n> ${prompt.slice(0, 100).replace(/\n/g, ' ')}...`
|
|
253
292
|
: prompt
|
|
254
293
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
294
|
+
const allFiles: Array<{ data: Uint8Array; filename: string; mimeType: string }> =
|
|
295
|
+
files.map((file) => ({
|
|
296
|
+
data: new Uint8Array(fs.readFileSync(file)),
|
|
297
|
+
filename: path.basename(file),
|
|
298
|
+
mimeType: mime.getType(file) || 'application/octet-stream',
|
|
299
|
+
}))
|
|
261
300
|
|
|
262
301
|
if (isLongPrompt) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
302
|
+
allFiles.push({
|
|
303
|
+
data: new Uint8Array(Buffer.from(prompt, 'utf8')),
|
|
304
|
+
filename: 'prompt.md',
|
|
305
|
+
mimeType: 'text/markdown',
|
|
306
|
+
})
|
|
267
307
|
}
|
|
268
308
|
|
|
269
309
|
const attachments = allFiles.map((f, index) => ({
|
|
@@ -283,10 +323,9 @@ export async function sendDiscordMessageWithOptionalAttachment({
|
|
|
283
323
|
)
|
|
284
324
|
|
|
285
325
|
for (const [index, f] of allFiles.entries()) {
|
|
286
|
-
const buffer = fs.readFileSync(f.filePath)
|
|
287
326
|
formData.append(
|
|
288
327
|
`files[${index}]`,
|
|
289
|
-
new Blob([
|
|
328
|
+
new Blob([f.data], { type: f.mimeType }),
|
|
290
329
|
f.filename,
|
|
291
330
|
)
|
|
292
331
|
}
|
|
@@ -344,82 +383,42 @@ export async function sendDiscordMessageWithOptionalAttachment({
|
|
|
344
383
|
|
|
345
384
|
const preview = prompt.slice(0, 100).replace(/\n/g, ' ')
|
|
346
385
|
const summaryContent = `Prompt attached as file (${prompt.length} chars)\n\n> ${preview}...`
|
|
386
|
+
// In-memory Blob only — no temp file. Parallel send must never share a path.
|
|
387
|
+
const formData = new FormData()
|
|
388
|
+
formData.append(
|
|
389
|
+
'payload_json',
|
|
390
|
+
JSON.stringify({
|
|
391
|
+
content: summaryContent,
|
|
392
|
+
attachments: [{ id: 0, filename: 'prompt.md' }],
|
|
393
|
+
embeds,
|
|
394
|
+
allowed_mentions: { parse: store.getState().allowedMentions },
|
|
395
|
+
}),
|
|
396
|
+
)
|
|
397
|
+
formData.append(
|
|
398
|
+
'files[0]',
|
|
399
|
+
promptAttachmentBlob(wrapPromptAttachmentText(prompt)),
|
|
400
|
+
'prompt.md',
|
|
401
|
+
)
|
|
347
402
|
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
.flatMap((line) => {
|
|
355
|
-
if (line.length <= 120) {
|
|
356
|
-
return [line]
|
|
357
|
-
}
|
|
358
|
-
const wrapped: string[] = []
|
|
359
|
-
let remaining = line
|
|
360
|
-
const maxCol = 120
|
|
361
|
-
// Only soft-break at a space if it's reasonably close to maxCol,
|
|
362
|
-
// otherwise hard-break to avoid tiny fragments from early spaces
|
|
363
|
-
const minSoftBreak = 90
|
|
364
|
-
while (remaining.length > maxCol) {
|
|
365
|
-
const lastSpace = remaining.lastIndexOf(' ', maxCol)
|
|
366
|
-
const useSoftBreak = lastSpace >= minSoftBreak
|
|
367
|
-
const breakAt = useSoftBreak ? lastSpace : maxCol
|
|
368
|
-
wrapped.push(remaining.slice(0, breakAt))
|
|
369
|
-
// Only consume the separator space on soft breaks
|
|
370
|
-
remaining = useSoftBreak
|
|
371
|
-
? remaining.slice(breakAt + 1)
|
|
372
|
-
: remaining.slice(breakAt)
|
|
373
|
-
}
|
|
374
|
-
if (remaining.length > 0) {
|
|
375
|
-
wrapped.push(remaining)
|
|
376
|
-
}
|
|
377
|
-
return wrapped
|
|
378
|
-
})
|
|
379
|
-
.join('\n')
|
|
380
|
-
fs.writeFileSync(tmpFile, wrappedPrompt)
|
|
381
|
-
|
|
382
|
-
try {
|
|
383
|
-
const formData = new FormData()
|
|
384
|
-
formData.append(
|
|
385
|
-
'payload_json',
|
|
386
|
-
JSON.stringify({
|
|
387
|
-
content: summaryContent,
|
|
388
|
-
attachments: [{ id: 0, filename: 'prompt.md' }],
|
|
389
|
-
embeds,
|
|
390
|
-
allowed_mentions: { parse: store.getState().allowedMentions },
|
|
391
|
-
}),
|
|
392
|
-
)
|
|
393
|
-
const buffer = fs.readFileSync(tmpFile)
|
|
394
|
-
formData.append(
|
|
395
|
-
'files[0]',
|
|
396
|
-
new Blob([buffer], { type: 'text/markdown' }),
|
|
397
|
-
'prompt.md',
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
const starterMessageResponse = await fetch(
|
|
401
|
-
discordApiUrl(`/channels/${channelId}/messages`),
|
|
402
|
-
{
|
|
403
|
-
method: 'POST',
|
|
404
|
-
headers: {
|
|
405
|
-
Authorization: `Bot ${botToken}`,
|
|
406
|
-
},
|
|
407
|
-
body: formData,
|
|
403
|
+
const starterMessageResponse = await fetch(
|
|
404
|
+
discordApiUrl(`/channels/${channelId}/messages`),
|
|
405
|
+
{
|
|
406
|
+
method: 'POST',
|
|
407
|
+
headers: {
|
|
408
|
+
Authorization: `Bot ${botToken}`,
|
|
408
409
|
},
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const error = await starterMessageResponse.text()
|
|
413
|
-
throw new Error(
|
|
414
|
-
`Discord API error: ${starterMessageResponse.status} - ${error}`,
|
|
415
|
-
)
|
|
416
|
-
}
|
|
410
|
+
body: formData,
|
|
411
|
+
},
|
|
412
|
+
)
|
|
417
413
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
414
|
+
if (!starterMessageResponse.ok) {
|
|
415
|
+
const error = await starterMessageResponse.text()
|
|
416
|
+
throw new Error(
|
|
417
|
+
`Discord API error: ${starterMessageResponse.status} - ${error}`,
|
|
418
|
+
)
|
|
422
419
|
}
|
|
420
|
+
|
|
421
|
+
return (await starterMessageResponse.json()) as { id: string }
|
|
423
422
|
}
|
|
424
423
|
|
|
425
424
|
export function formatRelativeTime(target: Date): string {
|
|
@@ -465,7 +464,7 @@ export type GuildMemberSearchResult = {
|
|
|
465
464
|
|
|
466
465
|
export type DiscordUserTarget = {
|
|
467
466
|
id: string
|
|
468
|
-
username
|
|
467
|
+
username?: string
|
|
469
468
|
}
|
|
470
469
|
|
|
471
470
|
export function isGuildMemberSearchResult(value: object | null): value is GuildMemberSearchResult {
|
|
@@ -615,7 +614,7 @@ export async function resolveDiscordUserOption({
|
|
|
615
614
|
const directUserId = getDiscordUserIdFromUserOption(user)
|
|
616
615
|
if (directUserId) {
|
|
617
616
|
cliLogger.log(`Using Discord user ID: ${directUserId}`)
|
|
618
|
-
return { id: directUserId
|
|
617
|
+
return { id: directUserId }
|
|
619
618
|
}
|
|
620
619
|
|
|
621
620
|
cliLogger.log(`Searching for user "${user}" in guild...`)
|
package/src/commands/agent.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ChannelType,
|
|
12
12
|
ThreadAutoArchiveDuration,
|
|
13
13
|
type ThreadChannel,
|
|
14
|
+
type TextChannel,
|
|
14
15
|
MessageFlags,
|
|
15
16
|
} from 'discord.js'
|
|
16
17
|
import crypto from 'node:crypto'
|
|
@@ -32,7 +33,11 @@ import {
|
|
|
32
33
|
} from '../discord-utils.js'
|
|
33
34
|
import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js'
|
|
34
35
|
import { createLogger, LogPrefix } from '../logger.js'
|
|
35
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
formatModelSource,
|
|
38
|
+
getCurrentModelInfo,
|
|
39
|
+
type CurrentModelInfo,
|
|
40
|
+
} from './model.js'
|
|
36
41
|
import { isGitRepositoryRoot } from '../worktrees.js'
|
|
37
42
|
import {
|
|
38
43
|
formatAutoWorktreeName,
|
|
@@ -45,24 +50,18 @@ import { store } from '../store.js'
|
|
|
45
50
|
const agentLogger = createLogger(LogPrefix.AGENT)
|
|
46
51
|
|
|
47
52
|
const AGENT_CONTEXT_TTL_MS = 10 * 60 * 1000
|
|
48
|
-
const pendingAgentContexts = new Map<
|
|
49
|
-
string,
|
|
50
|
-
{
|
|
51
|
-
dir: string
|
|
52
|
-
channelId: string
|
|
53
|
-
sessionId?: string
|
|
54
|
-
isThread: boolean
|
|
55
|
-
}
|
|
56
|
-
>()
|
|
53
|
+
const pendingAgentContexts = new Map<string, AgentCommandContext>()
|
|
57
54
|
|
|
58
55
|
/**
|
|
59
56
|
* Context for agent commands, containing channel/session info.
|
|
60
57
|
*/
|
|
61
58
|
export type AgentCommandContext = {
|
|
62
59
|
dir: string
|
|
60
|
+
workingDirectory: string
|
|
63
61
|
channelId: string
|
|
64
62
|
sessionId?: string
|
|
65
63
|
isThread: boolean
|
|
64
|
+
appId: string
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
export type CurrentAgentInfo =
|
|
@@ -188,6 +187,7 @@ async function resolveQuickAgentNameFromInteraction({
|
|
|
188
187
|
*/
|
|
189
188
|
export async function resolveAgentCommandContext({
|
|
190
189
|
interaction,
|
|
190
|
+
appId,
|
|
191
191
|
}: {
|
|
192
192
|
interaction: ChatInputCommandInteraction
|
|
193
193
|
appId: string
|
|
@@ -207,41 +207,40 @@ export async function resolveAgentCommandContext({
|
|
|
207
207
|
ChannelType.AnnouncementThread,
|
|
208
208
|
].includes(channel.type)
|
|
209
209
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (isThread) {
|
|
215
|
-
const thread = channel as ThreadChannel
|
|
216
|
-
const textChannel = await resolveTextChannel(thread)
|
|
217
|
-
const metadata = await getKimakiMetadata(textChannel)
|
|
218
|
-
projectDirectory = metadata.projectDirectory
|
|
219
|
-
targetChannelId = textChannel?.id || channel.id
|
|
220
|
-
|
|
221
|
-
sessionId = await getThreadSession(thread.id)
|
|
222
|
-
} else if (channel.type === ChannelType.GuildText) {
|
|
223
|
-
const metadata = await getKimakiMetadata(channel)
|
|
224
|
-
projectDirectory = metadata.projectDirectory
|
|
225
|
-
targetChannelId = channel.id
|
|
226
|
-
} else {
|
|
210
|
+
if (
|
|
211
|
+
!isThread &&
|
|
212
|
+
channel.type !== ChannelType.GuildText
|
|
213
|
+
) {
|
|
227
214
|
await interaction.editReply({
|
|
228
215
|
content: 'This command can only be used in text channels or threads',
|
|
229
216
|
})
|
|
230
217
|
return null
|
|
231
218
|
}
|
|
232
219
|
|
|
233
|
-
|
|
220
|
+
const resolved = await resolveWorkingDirectory({
|
|
221
|
+
channel: channel as TextChannel | ThreadChannel,
|
|
222
|
+
})
|
|
223
|
+
if (!resolved) {
|
|
234
224
|
await interaction.editReply({
|
|
235
225
|
content: 'This channel is not configured with a project directory',
|
|
236
226
|
})
|
|
237
227
|
return null
|
|
238
228
|
}
|
|
239
229
|
|
|
230
|
+
const textChannel = isThread
|
|
231
|
+
? await resolveTextChannel(channel as ThreadChannel)
|
|
232
|
+
: undefined
|
|
233
|
+
const sessionId = isThread
|
|
234
|
+
? await getThreadSession(channel.id)
|
|
235
|
+
: undefined
|
|
236
|
+
|
|
240
237
|
return {
|
|
241
|
-
dir: projectDirectory,
|
|
242
|
-
|
|
238
|
+
dir: resolved.projectDirectory,
|
|
239
|
+
workingDirectory: resolved.workingDirectory,
|
|
240
|
+
channelId: textChannel?.id || channel.id,
|
|
243
241
|
sessionId,
|
|
244
242
|
isThread,
|
|
243
|
+
appId,
|
|
245
244
|
}
|
|
246
245
|
}
|
|
247
246
|
|
|
@@ -270,6 +269,60 @@ export async function setAgentForContext({
|
|
|
270
269
|
}
|
|
271
270
|
}
|
|
272
271
|
|
|
272
|
+
function formatAgentModelLine(modelInfo: CurrentModelInfo): string {
|
|
273
|
+
if (modelInfo.type === 'none') return ''
|
|
274
|
+
const source = formatModelSource({
|
|
275
|
+
type: modelInfo.type,
|
|
276
|
+
agentName: modelInfo.type === 'agent' ? modelInfo.agentName : undefined,
|
|
277
|
+
})
|
|
278
|
+
const line = `\nModel: *${modelInfo.model}* (${source})`
|
|
279
|
+
if (modelInfo.type !== 'session' && modelInfo.type !== 'channel') {
|
|
280
|
+
return line
|
|
281
|
+
}
|
|
282
|
+
return `${line}\nThis model comes from a ${modelInfo.type} override. Use /model and press Clear override if you want this agent's model.`
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function formatAgentPreferenceReply({
|
|
286
|
+
agentName,
|
|
287
|
+
previousAgentName,
|
|
288
|
+
modelInfo,
|
|
289
|
+
scope,
|
|
290
|
+
}: {
|
|
291
|
+
agentName: string
|
|
292
|
+
previousAgentName?: string
|
|
293
|
+
modelInfo: CurrentModelInfo
|
|
294
|
+
scope: 'session' | 'channel'
|
|
295
|
+
}): string {
|
|
296
|
+
const sameAgent = previousAgentName === agentName
|
|
297
|
+
const verb = sameAgent ? 'Using' : 'Switched to'
|
|
298
|
+
const previousText =
|
|
299
|
+
!sameAgent && previousAgentName ? ` (was **${previousAgentName}**)` : ''
|
|
300
|
+
const modelText = formatAgentModelLine(modelInfo)
|
|
301
|
+
if (scope === 'session') {
|
|
302
|
+
return `${verb} **${agentName}** agent for this session${previousText}${modelText}\nThe agent will change on the next message.`
|
|
303
|
+
}
|
|
304
|
+
return `${verb} **${agentName}** agent for this channel${previousText}${modelText}\nAll new sessions will use this agent.`
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function resolveAgentModelInfo({
|
|
308
|
+
context,
|
|
309
|
+
agentName,
|
|
310
|
+
}: {
|
|
311
|
+
context: AgentCommandContext
|
|
312
|
+
agentName: string
|
|
313
|
+
}): Promise<CurrentModelInfo> {
|
|
314
|
+
const getClient = await initializeOpencodeForDirectory(context.dir)
|
|
315
|
+
if (getClient instanceof Error) return { type: 'none' }
|
|
316
|
+
return getCurrentModelInfo({
|
|
317
|
+
sessionId: context.sessionId,
|
|
318
|
+
channelId: context.channelId,
|
|
319
|
+
appId: context.appId,
|
|
320
|
+
agentPreference: agentName,
|
|
321
|
+
getClient,
|
|
322
|
+
directory: context.workingDirectory,
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
|
|
273
326
|
export async function handleAgentCommand({
|
|
274
327
|
interaction,
|
|
275
328
|
appId,
|
|
@@ -292,7 +345,7 @@ export async function handleAgentCommand({
|
|
|
292
345
|
}
|
|
293
346
|
|
|
294
347
|
const agentsResponse = await getClient().app.agents({
|
|
295
|
-
directory: context.
|
|
348
|
+
directory: context.workingDirectory,
|
|
296
349
|
})
|
|
297
350
|
|
|
298
351
|
if (!agentsResponse.data || agentsResponse.data.length === 0) {
|
|
@@ -392,19 +445,30 @@ export async function handleAgentSelectMenu(
|
|
|
392
445
|
}
|
|
393
446
|
|
|
394
447
|
try {
|
|
448
|
+
const previousAgent = await getCurrentAgentInfo({
|
|
449
|
+
sessionId: context.sessionId,
|
|
450
|
+
channelId: context.channelId,
|
|
451
|
+
})
|
|
452
|
+
const previousAgentName =
|
|
453
|
+
previousAgent.type !== 'none' ? previousAgent.agent : undefined
|
|
454
|
+
|
|
395
455
|
await setAgentForContext({ context, agentName: selectedAgent })
|
|
456
|
+
const modelInfo = await resolveAgentModelInfo({
|
|
457
|
+
context,
|
|
458
|
+
agentName: selectedAgent,
|
|
459
|
+
})
|
|
460
|
+
const scope =
|
|
461
|
+
context.isThread && context.sessionId ? 'session' : 'channel'
|
|
396
462
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
})
|
|
407
|
-
}
|
|
463
|
+
await interaction.editReply({
|
|
464
|
+
content: formatAgentPreferenceReply({
|
|
465
|
+
agentName: selectedAgent,
|
|
466
|
+
previousAgentName,
|
|
467
|
+
modelInfo,
|
|
468
|
+
scope,
|
|
469
|
+
}),
|
|
470
|
+
components: [],
|
|
471
|
+
})
|
|
408
472
|
|
|
409
473
|
pendingAgentContexts.delete(contextHash)
|
|
410
474
|
} catch (error) {
|
|
@@ -465,51 +529,23 @@ export async function handleQuickAgentCommand({
|
|
|
465
529
|
const previousAgentName =
|
|
466
530
|
previousAgent.type !== 'none' ? previousAgent.agent : undefined
|
|
467
531
|
|
|
468
|
-
if (previousAgentName === resolvedAgentName) {
|
|
469
|
-
await command.editReply({
|
|
470
|
-
content: `Already using **${resolvedAgentName}** agent`,
|
|
471
|
-
})
|
|
472
|
-
return
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Set the agent preference in DB for this context.
|
|
476
532
|
await setAgentForContext({ context, agentName: resolvedAgentName })
|
|
477
533
|
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
:
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
// model preference, so getCurrentModelInfo falls through to the agent's
|
|
485
|
-
// configured model (or channel/global/default).
|
|
486
|
-
const modelInfo = await (async () => {
|
|
487
|
-
const getClient = await initializeOpencodeForDirectory(context.dir)
|
|
488
|
-
if (getClient instanceof Error) {
|
|
489
|
-
return { type: 'none' as const }
|
|
490
|
-
}
|
|
491
|
-
return getCurrentModelInfo({
|
|
492
|
-
sessionId: context.sessionId,
|
|
493
|
-
channelId: context.channelId,
|
|
494
|
-
appId,
|
|
495
|
-
agentPreference: resolvedAgentName,
|
|
496
|
-
getClient,
|
|
497
|
-
directory: context.dir,
|
|
498
|
-
})
|
|
499
|
-
})()
|
|
500
|
-
|
|
501
|
-
const modelText =
|
|
502
|
-
modelInfo.type === 'none' ? '' : `\nModel: *${modelInfo.model}*`
|
|
534
|
+
const modelInfo = await resolveAgentModelInfo({
|
|
535
|
+
context,
|
|
536
|
+
agentName: resolvedAgentName,
|
|
537
|
+
})
|
|
538
|
+
const scope =
|
|
539
|
+
context.isThread && context.sessionId ? 'session' : 'channel'
|
|
503
540
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
541
|
+
await command.editReply({
|
|
542
|
+
content: formatAgentPreferenceReply({
|
|
543
|
+
agentName: resolvedAgentName,
|
|
544
|
+
previousAgentName,
|
|
545
|
+
modelInfo,
|
|
546
|
+
scope,
|
|
547
|
+
}),
|
|
548
|
+
})
|
|
513
549
|
} catch (error) {
|
|
514
550
|
agentLogger.error('Error in quick agent command:', error)
|
|
515
551
|
await command.editReply({
|
package/src/commands/model.ts
CHANGED
|
@@ -150,6 +150,31 @@ export type CurrentModelInfo =
|
|
|
150
150
|
}
|
|
151
151
|
| { type: 'none' }
|
|
152
152
|
|
|
153
|
+
export function formatModelSource({
|
|
154
|
+
type,
|
|
155
|
+
agentName,
|
|
156
|
+
}: {
|
|
157
|
+
type: CurrentModelInfo['type'] | string
|
|
158
|
+
agentName?: string
|
|
159
|
+
}): string {
|
|
160
|
+
switch (type) {
|
|
161
|
+
case 'session':
|
|
162
|
+
return 'session override'
|
|
163
|
+
case 'agent':
|
|
164
|
+
return `agent "${agentName}"`
|
|
165
|
+
case 'channel':
|
|
166
|
+
return 'channel override'
|
|
167
|
+
case 'global':
|
|
168
|
+
return 'global default'
|
|
169
|
+
case 'opencode-config':
|
|
170
|
+
case 'opencode-recent':
|
|
171
|
+
case 'opencode-provider-default':
|
|
172
|
+
return 'opencode default'
|
|
173
|
+
default:
|
|
174
|
+
return 'none'
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
153
178
|
function parseModelId(
|
|
154
179
|
modelString: string,
|
|
155
180
|
): { providerID: string; modelID: string } | undefined {
|