@brianli/kimaki 0.4.72-brianli.1
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/bin.js +2 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/bin.js +87 -0
- package/dist/bot-token.js +121 -0
- package/dist/bot-token.test.js +134 -0
- package/dist/channel-management.js +101 -0
- package/dist/cli-parsing.test.js +89 -0
- package/dist/cli.js +2529 -0
- package/dist/commands/abort.js +82 -0
- package/dist/commands/action-buttons.js +257 -0
- package/dist/commands/add-project.js +114 -0
- package/dist/commands/agent.js +291 -0
- package/dist/commands/ask-question.js +223 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +118 -0
- package/dist/commands/diff.js +128 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork.js +217 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +490 -0
- package/dist/commands/mention-mode.js +51 -0
- package/dist/commands/merge-worktree.js +124 -0
- package/dist/commands/model.js +694 -0
- package/dist/commands/permissions.js +163 -0
- package/dist/commands/queue.js +217 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +116 -0
- package/dist/commands/resume.js +159 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +192 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +159 -0
- package/dist/commands/unset-model.js +152 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +148 -0
- package/dist/commands/verbosity.js +60 -0
- package/dist/commands/worktree-settings.js +50 -0
- package/dist/commands/worktree.js +299 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +110 -0
- package/dist/database.js +1050 -0
- package/dist/db.js +159 -0
- package/dist/db.test.js +49 -0
- package/dist/discord-api.js +28 -0
- package/dist/discord-auth.js +231 -0
- package/dist/discord-auth.test.js +80 -0
- package/dist/discord-bot.js +997 -0
- package/dist/discord-utils.js +560 -0
- package/dist/discord-utils.test.js +115 -0
- package/dist/errors.js +167 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/format-tables.js +122 -0
- package/dist/format-tables.test.js +199 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +117 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-consumer.js +232 -0
- package/dist/gateway-consumer.test.js +18 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +35 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +30 -0
- package/dist/generated/internal/class.js +41 -0
- package/dist/generated/internal/prismaNamespace.js +239 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +209 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +95 -0
- package/dist/hrana-server.js +416 -0
- package/dist/hrana-server.test.js +368 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +327 -0
- package/dist/ipc-polling.js +251 -0
- package/dist/kimaki-digital-twin.e2e.test.js +165 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +160 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +253 -0
- package/dist/message-formatting.js +433 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/openai-realtime.js +228 -0
- package/dist/opencode-plugin-loading.e2e.test.js +91 -0
- package/dist/opencode-plugin.js +536 -0
- package/dist/opencode-plugin.test.js +98 -0
- package/dist/opencode.js +409 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/runtime-mode.js +51 -0
- package/dist/runtime-mode.test.js +115 -0
- package/dist/sentry.js +127 -0
- package/dist/session-handler/state.js +151 -0
- package/dist/session-handler.js +1874 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/startup-service.js +153 -0
- package/dist/system-message.js +499 -0
- package/dist/task-runner.js +282 -0
- package/dist/task-schedule.js +191 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +781 -0
- package/dist/tools.js +359 -0
- package/dist/unnest-code-blocks.js +136 -0
- package/dist/unnest-code-blocks.test.js +641 -0
- package/dist/upgrade.js +114 -0
- package/dist/utils.js +109 -0
- package/dist/voice-handler.js +606 -0
- package/dist/voice.js +304 -0
- package/dist/voice.test.js +187 -0
- package/dist/wait-session.js +94 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-utils.js +727 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +82 -0
- package/schema.prisma +246 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +129 -0
- package/skills/errore/SKILL.md +589 -0
- package/skills/goke/.prettierrc +5 -0
- package/skills/goke/CHANGELOG.md +40 -0
- package/skills/goke/LICENSE +21 -0
- package/skills/goke/README.md +666 -0
- package/skills/goke/SKILL.md +458 -0
- package/skills/goke/package.json +43 -0
- package/skills/goke/src/__test__/coerce.test.ts +411 -0
- package/skills/goke/src/__test__/index.test.ts +1798 -0
- package/skills/goke/src/__test__/types.test-d.ts +111 -0
- package/skills/goke/src/coerce.ts +547 -0
- package/skills/goke/src/goke.ts +1362 -0
- package/skills/goke/src/index.ts +16 -0
- package/skills/goke/src/mri.ts +164 -0
- package/skills/goke/tsconfig.json +15 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/playwriter/SKILL.md +31 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/zustand-centralized-state/SKILL.md +582 -0
- package/src/__snapshots__/compact-session-context-no-system.md +35 -0
- package/src/__snapshots__/compact-session-context.md +41 -0
- package/src/__snapshots__/first-session-no-info.md +17 -0
- package/src/__snapshots__/first-session-with-info.md +23 -0
- package/src/__snapshots__/session-1.md +17 -0
- package/src/__snapshots__/session-2.md +5871 -0
- package/src/__snapshots__/session-3.md +17 -0
- package/src/__snapshots__/session-with-tools.md +5871 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +282 -0
- package/src/ai-tool.ts +39 -0
- package/src/bin.ts +108 -0
- package/src/bot-token.test.ts +171 -0
- package/src/bot-token.ts +159 -0
- package/src/channel-management.ts +172 -0
- package/src/cli-parsing.test.ts +132 -0
- package/src/cli.ts +3605 -0
- package/src/commands/abort.ts +112 -0
- package/src/commands/action-buttons.ts +376 -0
- package/src/commands/add-project.ts +152 -0
- package/src/commands/agent.ts +404 -0
- package/src/commands/ask-question.ts +330 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +179 -0
- package/src/commands/diff.ts +165 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork.ts +320 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +634 -0
- package/src/commands/mention-mode.ts +77 -0
- package/src/commands/merge-worktree.ts +177 -0
- package/src/commands/model.ts +961 -0
- package/src/commands/permissions.ts +261 -0
- package/src/commands/queue.ts +296 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +242 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +250 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +221 -0
- package/src/commands/unset-model.ts +189 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +193 -0
- package/src/commands/verbosity.ts +88 -0
- package/src/commands/worktree-settings.ts +79 -0
- package/src/commands/worktree.ts +431 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +148 -0
- package/src/database.ts +1530 -0
- package/src/db.test.ts +60 -0
- package/src/db.ts +190 -0
- package/src/discord-api.ts +35 -0
- package/src/discord-bot.ts +1316 -0
- package/src/discord-utils.test.ts +132 -0
- package/src/discord-utils.ts +767 -0
- package/src/errors.ts +213 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/format-tables.test.ts +223 -0
- package/src/format-tables.ts +145 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +176 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +109 -0
- package/src/generated/client.ts +131 -0
- package/src/generated/commonInputTypes.ts +512 -0
- package/src/generated/enums.ts +46 -0
- package/src/generated/internal/class.ts +362 -0
- package/src/generated/internal/prismaNamespace.ts +2251 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +308 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1577 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +2104 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1224 -0
- package/src/generated/models/channel_worktrees.ts +1308 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1599 -0
- package/src/generated/models/thread_worktrees.ts +1352 -0
- package/src/generated/models.ts +29 -0
- package/src/heap-monitor.ts +121 -0
- package/src/hrana-server.test.ts +428 -0
- package/src/hrana-server.ts +547 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +461 -0
- package/src/ipc-polling.ts +325 -0
- package/src/kimaki-digital-twin.e2e.test.ts +201 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +203 -0
- package/src/markdown.test.ts +360 -0
- package/src/markdown.ts +410 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +549 -0
- package/src/openai-realtime.ts +362 -0
- package/src/opencode-plugin-loading.e2e.test.ts +112 -0
- package/src/opencode-plugin.test.ts +108 -0
- package/src/opencode-plugin.ts +652 -0
- package/src/opencode.ts +554 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/schema.sql +158 -0
- package/src/sentry.ts +137 -0
- package/src/session-handler/state.ts +232 -0
- package/src/session-handler.ts +2668 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/startup-service.ts +200 -0
- package/src/system-message.ts +568 -0
- package/src/task-runner.ts +425 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +287 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +997 -0
- package/src/tools.ts +432 -0
- package/src/unnest-code-blocks.test.ts +679 -0
- package/src/unnest-code-blocks.ts +168 -0
- package/src/upgrade.ts +127 -0
- package/src/utils.ts +145 -0
- package/src/voice-handler.ts +852 -0
- package/src/voice.test.ts +219 -0
- package/src/voice.ts +444 -0
- package/src/wait-session.ts +147 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-utils.ts +988 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// AskUserQuestion tool handler - Shows Discord dropdowns for AI questions.
|
|
2
|
+
// When the AI uses the AskUserQuestion tool, this module renders dropdowns
|
|
3
|
+
// for each question and collects user responses.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
StringSelectMenuBuilder,
|
|
7
|
+
StringSelectMenuInteraction,
|
|
8
|
+
ActionRowBuilder,
|
|
9
|
+
type ThreadChannel,
|
|
10
|
+
MessageFlags,
|
|
11
|
+
} from 'discord.js'
|
|
12
|
+
import crypto from 'node:crypto'
|
|
13
|
+
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
14
|
+
import { getOpencodeClient } from '../opencode.js'
|
|
15
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
16
|
+
|
|
17
|
+
const logger = createLogger(LogPrefix.ASK_QUESTION)
|
|
18
|
+
|
|
19
|
+
// Schema matching the question tool input
|
|
20
|
+
export type AskUserQuestionInput = {
|
|
21
|
+
questions: Array<{
|
|
22
|
+
question: string
|
|
23
|
+
header: string // max 12 chars
|
|
24
|
+
options: Array<{
|
|
25
|
+
label: string
|
|
26
|
+
description: string
|
|
27
|
+
}>
|
|
28
|
+
multiple?: boolean // optional, defaults to false
|
|
29
|
+
}>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type PendingQuestionContext = {
|
|
33
|
+
sessionId: string
|
|
34
|
+
directory: string
|
|
35
|
+
thread: ThreadChannel
|
|
36
|
+
requestId: string // OpenCode question request ID for replying
|
|
37
|
+
questions: AskUserQuestionInput['questions']
|
|
38
|
+
answers: Record<number, string[]> // questionIndex -> selected labels
|
|
39
|
+
totalQuestions: number
|
|
40
|
+
answeredCount: number
|
|
41
|
+
contextHash: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Store pending question contexts by hash
|
|
45
|
+
export const pendingQuestionContexts = new Map<string, PendingQuestionContext>()
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Show dropdown menus for question tool input.
|
|
49
|
+
* Sends one message per question with the dropdown directly under the question text.
|
|
50
|
+
*/
|
|
51
|
+
export async function showAskUserQuestionDropdowns({
|
|
52
|
+
thread,
|
|
53
|
+
sessionId,
|
|
54
|
+
directory,
|
|
55
|
+
requestId,
|
|
56
|
+
input,
|
|
57
|
+
}: {
|
|
58
|
+
thread: ThreadChannel
|
|
59
|
+
sessionId: string
|
|
60
|
+
directory: string
|
|
61
|
+
requestId: string // OpenCode question request ID
|
|
62
|
+
input: AskUserQuestionInput
|
|
63
|
+
}): Promise<void> {
|
|
64
|
+
const contextHash = crypto.randomBytes(8).toString('hex')
|
|
65
|
+
|
|
66
|
+
const context: PendingQuestionContext = {
|
|
67
|
+
sessionId,
|
|
68
|
+
directory,
|
|
69
|
+
thread,
|
|
70
|
+
requestId,
|
|
71
|
+
questions: input.questions,
|
|
72
|
+
answers: {},
|
|
73
|
+
totalQuestions: input.questions.length,
|
|
74
|
+
answeredCount: 0,
|
|
75
|
+
contextHash,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
pendingQuestionContexts.set(contextHash, context)
|
|
79
|
+
|
|
80
|
+
// Send one message per question with its dropdown directly underneath
|
|
81
|
+
for (let i = 0; i < input.questions.length; i++) {
|
|
82
|
+
const q = input.questions[i]!
|
|
83
|
+
|
|
84
|
+
// Map options to Discord select menu options
|
|
85
|
+
// Discord max: 25 options per select menu
|
|
86
|
+
const options = [
|
|
87
|
+
...q.options.slice(0, 24).map((opt, optIdx) => ({
|
|
88
|
+
label: opt.label.slice(0, 100),
|
|
89
|
+
value: `${optIdx}`,
|
|
90
|
+
description: opt.description.slice(0, 100),
|
|
91
|
+
})),
|
|
92
|
+
{
|
|
93
|
+
label: 'Other',
|
|
94
|
+
value: 'other',
|
|
95
|
+
description: 'Provide a custom answer in chat',
|
|
96
|
+
},
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
const placeholder =
|
|
100
|
+
options.find((x) => x.label)?.label || 'Select an option'
|
|
101
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
102
|
+
.setCustomId(`ask_question:${contextHash}:${i}`)
|
|
103
|
+
.setPlaceholder(placeholder)
|
|
104
|
+
.addOptions(options)
|
|
105
|
+
|
|
106
|
+
// Enable multi-select if the question supports it
|
|
107
|
+
if (q.multiple) {
|
|
108
|
+
selectMenu.setMinValues(1)
|
|
109
|
+
selectMenu.setMaxValues(options.length)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const actionRow =
|
|
113
|
+
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
|
|
114
|
+
|
|
115
|
+
await thread.send({
|
|
116
|
+
content: `**${(q.header || '').slice(0, 200)}**\n${q.question.slice(0, 1700)}`,
|
|
117
|
+
components: [actionRow],
|
|
118
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
logger.log(
|
|
123
|
+
`Showed ${input.questions.length} question dropdown(s) for session ${sessionId}`,
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Handle dropdown selection for AskUserQuestion.
|
|
129
|
+
*/
|
|
130
|
+
export async function handleAskQuestionSelectMenu(
|
|
131
|
+
interaction: StringSelectMenuInteraction,
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
const customId = interaction.customId
|
|
134
|
+
|
|
135
|
+
if (!customId.startsWith('ask_question:')) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const parts = customId.split(':')
|
|
140
|
+
const contextHash = parts[1]
|
|
141
|
+
const questionIndex = parseInt(parts[2]!, 10)
|
|
142
|
+
|
|
143
|
+
if (!contextHash) {
|
|
144
|
+
await interaction.reply({
|
|
145
|
+
content: 'Invalid selection.',
|
|
146
|
+
flags: MessageFlags.Ephemeral,
|
|
147
|
+
})
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const context = pendingQuestionContexts.get(contextHash)
|
|
152
|
+
|
|
153
|
+
if (!context) {
|
|
154
|
+
await interaction.reply({
|
|
155
|
+
content: 'This question has expired. Please ask the AI again.',
|
|
156
|
+
flags: MessageFlags.Ephemeral,
|
|
157
|
+
})
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await interaction.deferUpdate()
|
|
162
|
+
|
|
163
|
+
const selectedValues = interaction.values
|
|
164
|
+
const question = context.questions[questionIndex]
|
|
165
|
+
|
|
166
|
+
if (!question) {
|
|
167
|
+
logger.error(`Question index ${questionIndex} not found in context`)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check if "other" was selected
|
|
172
|
+
if (selectedValues.includes('other')) {
|
|
173
|
+
// User wants to provide custom answer
|
|
174
|
+
// For now, mark as "Other" - they can type in chat
|
|
175
|
+
context.answers[questionIndex] = ['Other (please type your answer in chat)']
|
|
176
|
+
} else {
|
|
177
|
+
// Map value indices back to option labels
|
|
178
|
+
context.answers[questionIndex] = selectedValues.map((v) => {
|
|
179
|
+
const optIdx = parseInt(v, 10)
|
|
180
|
+
return question.options[optIdx]?.label || `Option ${optIdx + 1}`
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
context.answeredCount++
|
|
185
|
+
|
|
186
|
+
// Update this question's message: show answer and remove dropdown
|
|
187
|
+
const answeredText = context.answers[questionIndex]!.join(', ')
|
|
188
|
+
await interaction.editReply({
|
|
189
|
+
content: `**${question.header}**\n${question.question}\n✓ _${answeredText}_`,
|
|
190
|
+
components: [], // Remove the dropdown
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// Check if all questions are answered
|
|
194
|
+
if (context.answeredCount >= context.totalQuestions) {
|
|
195
|
+
// All questions answered - send result back to session
|
|
196
|
+
await submitQuestionAnswers(context)
|
|
197
|
+
pendingQuestionContexts.delete(contextHash)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Submit all collected answers back to the OpenCode session.
|
|
203
|
+
* Uses the question.reply API to provide answers to the waiting tool.
|
|
204
|
+
*/
|
|
205
|
+
async function submitQuestionAnswers(
|
|
206
|
+
context: PendingQuestionContext,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
try {
|
|
209
|
+
const client = getOpencodeClient(context.directory)
|
|
210
|
+
if (!client) {
|
|
211
|
+
throw new Error('OpenCode server not found for directory')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Build answers array: each element is an array of selected labels for that question
|
|
215
|
+
const answers = context.questions.map((_, i) => {
|
|
216
|
+
return context.answers[i] || []
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
await client.question.reply({
|
|
220
|
+
requestID: context.requestId,
|
|
221
|
+
directory: context.directory,
|
|
222
|
+
answers,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
logger.log(
|
|
226
|
+
`Submitted answers for question ${context.requestId} in session ${context.sessionId}`,
|
|
227
|
+
)
|
|
228
|
+
} catch (error) {
|
|
229
|
+
logger.error('Failed to submit answers:', error)
|
|
230
|
+
await sendThreadMessage(
|
|
231
|
+
context.thread,
|
|
232
|
+
`✗ Failed to submit answers: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Check if a tool part is an AskUserQuestion tool.
|
|
239
|
+
* Returns the parsed input if valid, null otherwise.
|
|
240
|
+
*/
|
|
241
|
+
export function parseAskUserQuestionTool(part: {
|
|
242
|
+
type: string
|
|
243
|
+
tool?: string
|
|
244
|
+
state?: { input?: unknown }
|
|
245
|
+
}): AskUserQuestionInput | null {
|
|
246
|
+
if (part.type !== 'tool') {
|
|
247
|
+
return null
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check for the tool name (case-insensitive)
|
|
251
|
+
const toolName = part.tool?.toLowerCase()
|
|
252
|
+
if (toolName !== 'question') {
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const input = part.state?.input as AskUserQuestionInput | undefined
|
|
257
|
+
|
|
258
|
+
if (
|
|
259
|
+
!input?.questions ||
|
|
260
|
+
!Array.isArray(input.questions) ||
|
|
261
|
+
input.questions.length === 0
|
|
262
|
+
) {
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Validate structure
|
|
267
|
+
for (const q of input.questions) {
|
|
268
|
+
if (
|
|
269
|
+
typeof q.question !== 'string' ||
|
|
270
|
+
typeof q.header !== 'string' ||
|
|
271
|
+
!Array.isArray(q.options) ||
|
|
272
|
+
q.options.length < 2
|
|
273
|
+
) {
|
|
274
|
+
return null
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return input
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Cancel a pending question for a thread (e.g., when user sends a new message).
|
|
283
|
+
* Sends the user's message as the answer to OpenCode so the model sees their actual response.
|
|
284
|
+
*/
|
|
285
|
+
export async function cancelPendingQuestion(
|
|
286
|
+
threadId: string,
|
|
287
|
+
userMessage?: string,
|
|
288
|
+
): Promise<boolean> {
|
|
289
|
+
// Find pending question for this thread
|
|
290
|
+
let contextHash: string | undefined
|
|
291
|
+
let context: PendingQuestionContext | undefined
|
|
292
|
+
for (const [hash, ctx] of pendingQuestionContexts) {
|
|
293
|
+
if (ctx.thread.id === threadId) {
|
|
294
|
+
contextHash = hash
|
|
295
|
+
context = ctx
|
|
296
|
+
break
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!contextHash || !context) {
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const client = getOpencodeClient(context.directory)
|
|
306
|
+
if (!client) {
|
|
307
|
+
throw new Error('OpenCode server not found for directory')
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Use user's message as answer if provided, otherwise mark as "Other"
|
|
311
|
+
const customAnswer = userMessage || 'Other'
|
|
312
|
+
const answers = context.questions.map((_, i) => {
|
|
313
|
+
return context.answers[i] || [customAnswer]
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
await client.question.reply({
|
|
317
|
+
requestID: context.requestId,
|
|
318
|
+
directory: context.directory,
|
|
319
|
+
answers,
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
logger.log(`Answered question ${context.requestId} with user message`)
|
|
323
|
+
} catch (error) {
|
|
324
|
+
logger.error('Failed to answer question:', error)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Clean up regardless of whether the API call succeeded
|
|
328
|
+
pendingQuestionContexts.delete(contextHash)
|
|
329
|
+
return true
|
|
330
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// /compact command - Trigger context compaction (summarization) for the current session.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ChannelType,
|
|
5
|
+
MessageFlags,
|
|
6
|
+
type TextChannel,
|
|
7
|
+
type ThreadChannel,
|
|
8
|
+
} from 'discord.js'
|
|
9
|
+
import type { CommandContext } from './types.js'
|
|
10
|
+
import { getThreadSession } from '../database.js'
|
|
11
|
+
import {
|
|
12
|
+
initializeOpencodeForDirectory,
|
|
13
|
+
getOpencodeClient,
|
|
14
|
+
} from '../opencode.js'
|
|
15
|
+
import {
|
|
16
|
+
resolveWorkingDirectory,
|
|
17
|
+
SILENT_MESSAGE_FLAGS,
|
|
18
|
+
} from '../discord-utils.js'
|
|
19
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
20
|
+
|
|
21
|
+
const logger = createLogger(LogPrefix.COMPACT)
|
|
22
|
+
|
|
23
|
+
export async function handleCompactCommand({
|
|
24
|
+
command,
|
|
25
|
+
}: CommandContext): Promise<void> {
|
|
26
|
+
const channel = command.channel
|
|
27
|
+
|
|
28
|
+
if (!channel) {
|
|
29
|
+
await command.reply({
|
|
30
|
+
content: 'This command can only be used in a channel',
|
|
31
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
32
|
+
})
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isThread = [
|
|
37
|
+
ChannelType.PublicThread,
|
|
38
|
+
ChannelType.PrivateThread,
|
|
39
|
+
ChannelType.AnnouncementThread,
|
|
40
|
+
].includes(channel.type)
|
|
41
|
+
|
|
42
|
+
if (!isThread) {
|
|
43
|
+
await command.reply({
|
|
44
|
+
content:
|
|
45
|
+
'This command can only be used in a thread with an active session',
|
|
46
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
47
|
+
})
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const resolved = await resolveWorkingDirectory({
|
|
52
|
+
channel: channel as TextChannel | ThreadChannel,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (!resolved) {
|
|
56
|
+
await command.reply({
|
|
57
|
+
content: 'Could not determine project directory for this channel',
|
|
58
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
59
|
+
})
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { projectDirectory, workingDirectory } = resolved
|
|
64
|
+
|
|
65
|
+
const sessionId = await getThreadSession(channel.id)
|
|
66
|
+
|
|
67
|
+
if (!sessionId) {
|
|
68
|
+
await command.reply({
|
|
69
|
+
content: 'No active session in this thread',
|
|
70
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
71
|
+
})
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Ensure server is running for the base project directory
|
|
76
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
77
|
+
if (getClient instanceof Error) {
|
|
78
|
+
await command.reply({
|
|
79
|
+
content: `Failed to compact: ${getClient.message}`,
|
|
80
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
81
|
+
})
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const client = getOpencodeClient(projectDirectory)
|
|
86
|
+
if (!client) {
|
|
87
|
+
await command.reply({
|
|
88
|
+
content: 'Failed to get OpenCode client',
|
|
89
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
90
|
+
})
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Defer reply since compaction may take a moment
|
|
95
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Get session messages to find the model from the last user message
|
|
99
|
+
const messagesResult = await client.session.messages({
|
|
100
|
+
sessionID: sessionId,
|
|
101
|
+
directory: workingDirectory,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
if (messagesResult.error || !messagesResult.data) {
|
|
105
|
+
logger.error('[COMPACT] Failed to get messages:', messagesResult.error)
|
|
106
|
+
await command.editReply({
|
|
107
|
+
content: 'Failed to compact: Could not retrieve session messages',
|
|
108
|
+
})
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Find the last user message to get the model
|
|
113
|
+
const lastUserMessage = [...messagesResult.data]
|
|
114
|
+
.reverse()
|
|
115
|
+
.find((msg) => msg.info.role === 'user')
|
|
116
|
+
|
|
117
|
+
if (!lastUserMessage || lastUserMessage.info.role !== 'user') {
|
|
118
|
+
await command.editReply({
|
|
119
|
+
content: 'Failed to compact: No user message found in session',
|
|
120
|
+
})
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { providerID, modelID } = lastUserMessage.info.model
|
|
125
|
+
|
|
126
|
+
const result = await client.session.summarize({
|
|
127
|
+
sessionID: sessionId,
|
|
128
|
+
directory: workingDirectory,
|
|
129
|
+
providerID,
|
|
130
|
+
modelID,
|
|
131
|
+
auto: false,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
if (result.error) {
|
|
135
|
+
logger.error('[COMPACT] Error:', result.error)
|
|
136
|
+
const errorMessage =
|
|
137
|
+
'data' in result.error && result.error.data
|
|
138
|
+
? (result.error.data as { message?: string }).message ||
|
|
139
|
+
'Unknown error'
|
|
140
|
+
: 'Unknown error'
|
|
141
|
+
await command.editReply({
|
|
142
|
+
content: `Failed to compact: ${errorMessage}`,
|
|
143
|
+
})
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await command.editReply({
|
|
148
|
+
content: `📦 Session **compacted** successfully`,
|
|
149
|
+
})
|
|
150
|
+
logger.log(`Session ${sessionId} compacted by user`)
|
|
151
|
+
} catch (error) {
|
|
152
|
+
logger.error('[COMPACT] Error:', error)
|
|
153
|
+
await command.editReply({
|
|
154
|
+
content: `Failed to compact: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// /context-usage command - Show token usage and context window percentage for the current session.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ChannelType,
|
|
5
|
+
MessageFlags,
|
|
6
|
+
type TextChannel,
|
|
7
|
+
type ThreadChannel,
|
|
8
|
+
} from 'discord.js'
|
|
9
|
+
import type { CommandContext } from './types.js'
|
|
10
|
+
import { getThreadSession } from '../database.js'
|
|
11
|
+
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
12
|
+
import {
|
|
13
|
+
resolveWorkingDirectory,
|
|
14
|
+
SILENT_MESSAGE_FLAGS,
|
|
15
|
+
} from '../discord-utils.js'
|
|
16
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
17
|
+
import * as errore from 'errore'
|
|
18
|
+
|
|
19
|
+
const logger = createLogger(LogPrefix.SESSION)
|
|
20
|
+
|
|
21
|
+
function getTokenTotal({
|
|
22
|
+
input,
|
|
23
|
+
output,
|
|
24
|
+
reasoning,
|
|
25
|
+
cache,
|
|
26
|
+
}: {
|
|
27
|
+
input: number
|
|
28
|
+
output: number
|
|
29
|
+
reasoning: number
|
|
30
|
+
cache: { read: number; write: number }
|
|
31
|
+
}): number {
|
|
32
|
+
return input + output + reasoning + cache.read + cache.write
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function handleContextUsageCommand({
|
|
36
|
+
command,
|
|
37
|
+
}: CommandContext): Promise<void> {
|
|
38
|
+
const channel = command.channel
|
|
39
|
+
|
|
40
|
+
if (!channel) {
|
|
41
|
+
await command.reply({
|
|
42
|
+
content: 'This command can only be used in a channel',
|
|
43
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
44
|
+
})
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isThread = [
|
|
49
|
+
ChannelType.PublicThread,
|
|
50
|
+
ChannelType.PrivateThread,
|
|
51
|
+
ChannelType.AnnouncementThread,
|
|
52
|
+
].includes(channel.type)
|
|
53
|
+
|
|
54
|
+
if (!isThread) {
|
|
55
|
+
await command.reply({
|
|
56
|
+
content:
|
|
57
|
+
'This command can only be used in a thread with an active session',
|
|
58
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
59
|
+
})
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const resolved = await resolveWorkingDirectory({
|
|
64
|
+
channel: channel as TextChannel | ThreadChannel,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if (!resolved) {
|
|
68
|
+
await command.reply({
|
|
69
|
+
content: 'Could not determine project directory for this channel',
|
|
70
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
71
|
+
})
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { projectDirectory, workingDirectory } = resolved
|
|
76
|
+
|
|
77
|
+
const sessionId = await getThreadSession(channel.id)
|
|
78
|
+
|
|
79
|
+
if (!sessionId) {
|
|
80
|
+
await command.reply({
|
|
81
|
+
content: 'No active session in this thread',
|
|
82
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
83
|
+
})
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
88
|
+
if (getClient instanceof Error) {
|
|
89
|
+
await command.reply({
|
|
90
|
+
content: `Failed to get context usage: ${getClient.message}`,
|
|
91
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
92
|
+
})
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const messagesResponse = await getClient().session.messages({
|
|
100
|
+
sessionID: sessionId,
|
|
101
|
+
directory: workingDirectory,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const messages = messagesResponse.data || []
|
|
105
|
+
const assistantMessages = messages.filter(
|
|
106
|
+
(m) => m.info.role === 'assistant',
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if (assistantMessages.length === 0) {
|
|
110
|
+
await command.editReply({
|
|
111
|
+
content: 'No assistant messages in this session yet',
|
|
112
|
+
})
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const lastAssistant = [...assistantMessages].reverse().find((m) => {
|
|
117
|
+
if (m.info.role !== 'assistant') {
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
if (!('tokens' in m.info) || !m.info.tokens) {
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
return getTokenTotal(m.info.tokens) > 0
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
if (!lastAssistant || lastAssistant.info.role !== 'assistant') {
|
|
127
|
+
await command.editReply({
|
|
128
|
+
content: 'Token usage not available for this session yet',
|
|
129
|
+
})
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { tokens, modelID, providerID } = lastAssistant.info
|
|
134
|
+
const totalTokens = getTokenTotal(tokens)
|
|
135
|
+
|
|
136
|
+
// Sum cost across all assistant messages for accurate session total
|
|
137
|
+
// (AssistantMessage.cost is per-message, not cumulative)
|
|
138
|
+
const totalCost = assistantMessages.reduce((sum, m) => {
|
|
139
|
+
if (m.info.role === 'assistant' && 'cost' in m.info) {
|
|
140
|
+
return sum + (m.info.cost || 0)
|
|
141
|
+
}
|
|
142
|
+
return sum
|
|
143
|
+
}, 0)
|
|
144
|
+
|
|
145
|
+
// Fetch model context limit from provider API
|
|
146
|
+
let contextLimit: number | undefined
|
|
147
|
+
const providersResult = await errore.tryAsync(() => {
|
|
148
|
+
return getClient().provider.list({ directory: workingDirectory })
|
|
149
|
+
})
|
|
150
|
+
if (providersResult instanceof Error) {
|
|
151
|
+
logger.error(
|
|
152
|
+
'[CONTEXT-USAGE] Failed to fetch provider info:',
|
|
153
|
+
providersResult,
|
|
154
|
+
)
|
|
155
|
+
} else {
|
|
156
|
+
const provider = providersResult.data?.all?.find(
|
|
157
|
+
(p) => p.id === providerID,
|
|
158
|
+
)
|
|
159
|
+
const model = provider?.models?.[modelID]
|
|
160
|
+
if (model?.limit?.context) {
|
|
161
|
+
contextLimit = model.limit.context
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const formattedTokens = totalTokens.toLocaleString('en-US')
|
|
166
|
+
const formattedCost = totalCost > 0 ? `$${totalCost.toFixed(4)}` : '$0.00'
|
|
167
|
+
|
|
168
|
+
const lines: string[] = []
|
|
169
|
+
|
|
170
|
+
if (contextLimit) {
|
|
171
|
+
const percentage = Math.round((totalTokens / contextLimit) * 100)
|
|
172
|
+
const formattedLimit = contextLimit.toLocaleString('en-US')
|
|
173
|
+
lines.push(
|
|
174
|
+
`**Context usage:** ${percentage}%, ${formattedTokens} / ${formattedLimit} tokens`,
|
|
175
|
+
)
|
|
176
|
+
} else {
|
|
177
|
+
lines.push(
|
|
178
|
+
`**Context usage:** ${formattedTokens} tokens (context limit unavailable)`,
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (modelID) {
|
|
183
|
+
lines.push(`**Model:** ${modelID}`)
|
|
184
|
+
}
|
|
185
|
+
if (totalCost > 0) {
|
|
186
|
+
lines.push(`**Session cost:** ${formattedCost}`)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await command.editReply({ content: lines.join('\n') })
|
|
190
|
+
logger.log(
|
|
191
|
+
`Context usage shown for session ${sessionId}: ${totalTokens} tokens`,
|
|
192
|
+
)
|
|
193
|
+
} catch (error) {
|
|
194
|
+
logger.error('[CONTEXT-USAGE] Error:', error)
|
|
195
|
+
await command.editReply({
|
|
196
|
+
content: `Failed to get context usage: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
}
|