kimaki 0.4.37 → 0.4.39
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/channel-management.js +6 -2
- package/dist/cli.js +41 -15
- package/dist/commands/abort.js +15 -6
- package/dist/commands/add-project.js +9 -0
- package/dist/commands/agent.js +114 -20
- package/dist/commands/fork.js +13 -2
- package/dist/commands/model.js +12 -0
- package/dist/commands/remove-project.js +26 -16
- package/dist/commands/resume.js +9 -0
- package/dist/commands/session.js +13 -0
- package/dist/commands/share.js +10 -1
- package/dist/commands/undo-redo.js +13 -4
- package/dist/database.js +24 -5
- package/dist/discord-bot.js +38 -31
- package/dist/errors.js +110 -0
- package/dist/genai-worker.js +18 -16
- package/dist/interaction-handler.js +6 -1
- package/dist/markdown.js +96 -85
- package/dist/markdown.test.js +10 -3
- package/dist/message-formatting.js +50 -37
- package/dist/opencode.js +43 -46
- package/dist/session-handler.js +136 -8
- package/dist/system-message.js +2 -0
- package/dist/tools.js +18 -8
- package/dist/voice-handler.js +48 -25
- package/dist/voice.js +159 -131
- package/package.json +2 -1
- package/src/channel-management.ts +6 -2
- package/src/cli.ts +67 -19
- package/src/commands/abort.ts +17 -7
- package/src/commands/add-project.ts +9 -0
- package/src/commands/agent.ts +160 -25
- package/src/commands/fork.ts +18 -7
- package/src/commands/model.ts +12 -0
- package/src/commands/remove-project.ts +28 -16
- package/src/commands/resume.ts +9 -0
- package/src/commands/session.ts +13 -0
- package/src/commands/share.ts +11 -1
- package/src/commands/undo-redo.ts +15 -6
- package/src/database.ts +26 -4
- package/src/discord-bot.ts +42 -34
- package/src/errors.ts +208 -0
- package/src/genai-worker.ts +20 -17
- package/src/interaction-handler.ts +7 -1
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +111 -95
- package/src/message-formatting.ts +55 -38
- package/src/opencode.ts +52 -49
- package/src/session-handler.ts +164 -11
- package/src/system-message.ts +2 -0
- package/src/tools.ts +18 -8
- package/src/voice-handler.ts +48 -23
- package/src/voice.ts +195 -148
package/src/discord-bot.ts
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
type ThreadChannel,
|
|
54
54
|
} from 'discord.js'
|
|
55
55
|
import fs from 'node:fs'
|
|
56
|
+
import * as errore from 'errore'
|
|
56
57
|
import { extractTagsArrays } from './xml.js'
|
|
57
58
|
import { createLogger } from './logger.js'
|
|
58
59
|
import { setGlobalDispatcher, Agent } from 'undici'
|
|
@@ -149,10 +150,12 @@ export async function startDiscordBot({
|
|
|
149
150
|
}
|
|
150
151
|
if (message.partial) {
|
|
151
152
|
discordLogger.log(`Fetching partial message ${message.id}`)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
const fetched = await errore.tryAsync({
|
|
154
|
+
try: () => message.fetch(),
|
|
155
|
+
catch: (e) => e as Error,
|
|
156
|
+
})
|
|
157
|
+
if (errore.isError(fetched)) {
|
|
158
|
+
discordLogger.log(`Failed to fetch partial message ${message.id}:`, fetched.message)
|
|
156
159
|
return
|
|
157
160
|
}
|
|
158
161
|
}
|
|
@@ -230,17 +233,11 @@ export async function startDiscordBot({
|
|
|
230
233
|
return
|
|
231
234
|
}
|
|
232
235
|
|
|
233
|
-
// Include starter message
|
|
236
|
+
// Include starter message as context for the session
|
|
234
237
|
let prompt = message.content
|
|
235
238
|
const starterMessage = await thread.fetchStarterMessage().catch(() => null)
|
|
236
|
-
if (starterMessage?.content) {
|
|
237
|
-
|
|
238
|
-
const notificationContent = starterMessage.content
|
|
239
|
-
.replace(/^📢 \*\*Notification\*\*\n?/, '')
|
|
240
|
-
.trim()
|
|
241
|
-
if (notificationContent) {
|
|
242
|
-
prompt = `Context from notification:\n${notificationContent}\n\nUser request:\n${message.content}`
|
|
243
|
-
}
|
|
239
|
+
if (starterMessage?.content && starterMessage.content !== message.content) {
|
|
240
|
+
prompt = `Context from thread:\n${starterMessage.content}\n\nUser request:\n${message.content}`
|
|
244
241
|
}
|
|
245
242
|
|
|
246
243
|
await handleOpencodeSession({
|
|
@@ -262,30 +259,41 @@ export async function startDiscordBot({
|
|
|
262
259
|
if (projectDirectory) {
|
|
263
260
|
try {
|
|
264
261
|
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
262
|
+
if (errore.isError(getClient)) {
|
|
263
|
+
voiceLogger.error(`[SESSION] Failed to initialize OpenCode client:`, getClient.message)
|
|
264
|
+
throw new Error(getClient.message)
|
|
265
|
+
}
|
|
265
266
|
const client = getClient()
|
|
266
267
|
|
|
267
268
|
// get current session context (without system prompt, it would be duplicated)
|
|
268
269
|
if (row.session_id) {
|
|
269
|
-
|
|
270
|
+
const result = await getCompactSessionContext({
|
|
270
271
|
client,
|
|
271
272
|
sessionId: row.session_id,
|
|
272
273
|
includeSystemPrompt: false,
|
|
273
274
|
maxMessages: 15,
|
|
274
275
|
})
|
|
276
|
+
if (errore.isOk(result)) {
|
|
277
|
+
currentSessionContext = result
|
|
278
|
+
}
|
|
275
279
|
}
|
|
276
280
|
|
|
277
281
|
// get last session context (with system prompt for project context)
|
|
278
|
-
const
|
|
282
|
+
const lastSessionResult = await getLastSessionId({
|
|
279
283
|
client,
|
|
280
284
|
excludeSessionId: row.session_id,
|
|
281
285
|
})
|
|
286
|
+
const lastSessionId = errore.unwrapOr(lastSessionResult, null)
|
|
282
287
|
if (lastSessionId) {
|
|
283
|
-
|
|
288
|
+
const result = await getCompactSessionContext({
|
|
284
289
|
client,
|
|
285
290
|
sessionId: lastSessionId,
|
|
286
291
|
includeSystemPrompt: true,
|
|
287
292
|
maxMessages: 10,
|
|
288
293
|
})
|
|
294
|
+
if (errore.isOk(result)) {
|
|
295
|
+
lastSessionContext = result
|
|
296
|
+
}
|
|
289
297
|
}
|
|
290
298
|
} catch (e) {
|
|
291
299
|
voiceLogger.error(`Could not get session context:`, e)
|
|
@@ -419,42 +427,42 @@ export async function startDiscordBot({
|
|
|
419
427
|
}
|
|
420
428
|
})
|
|
421
429
|
|
|
422
|
-
//
|
|
423
|
-
const BOT_SESSION_PREFIX = '🤖 **Bot-initiated session**'
|
|
424
|
-
|
|
425
|
-
// Handle bot-initiated threads created by `kimaki send`
|
|
430
|
+
// Handle bot-initiated threads created by `kimaki send` (without --notify-only)
|
|
426
431
|
discordClient.on(Events.ThreadCreate, async (thread, newlyCreated) => {
|
|
427
432
|
try {
|
|
428
433
|
if (!newlyCreated) {
|
|
429
434
|
return
|
|
430
435
|
}
|
|
431
436
|
|
|
437
|
+
// Check if this thread is marked for auto-start in the database
|
|
438
|
+
const db = getDatabase()
|
|
439
|
+
const pendingRow = db
|
|
440
|
+
.prepare('SELECT thread_id FROM pending_auto_start WHERE thread_id = ?')
|
|
441
|
+
.get(thread.id) as { thread_id: string } | undefined
|
|
442
|
+
|
|
443
|
+
if (!pendingRow) {
|
|
444
|
+
return // Not a CLI-initiated auto-start thread
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Remove from pending table
|
|
448
|
+
db.prepare('DELETE FROM pending_auto_start WHERE thread_id = ?').run(thread.id)
|
|
449
|
+
|
|
450
|
+
discordLogger.log(`[BOT_SESSION] Detected bot-initiated thread: ${thread.name}`)
|
|
451
|
+
|
|
432
452
|
// Only handle threads in text channels
|
|
433
453
|
const parent = thread.parent as TextChannel | null
|
|
434
454
|
if (!parent || parent.type !== ChannelType.GuildText) {
|
|
435
455
|
return
|
|
436
456
|
}
|
|
437
457
|
|
|
438
|
-
// Get the starter message
|
|
458
|
+
// Get the starter message for the prompt
|
|
439
459
|
const starterMessage = await thread.fetchStarterMessage().catch(() => null)
|
|
440
460
|
if (!starterMessage) {
|
|
441
461
|
discordLogger.log(`[THREAD_CREATE] Could not fetch starter message for thread ${thread.id}`)
|
|
442
462
|
return
|
|
443
463
|
}
|
|
444
464
|
|
|
445
|
-
|
|
446
|
-
if (starterMessage.author.id !== discordClient.user?.id) {
|
|
447
|
-
return
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (!starterMessage.content.startsWith(BOT_SESSION_PREFIX)) {
|
|
451
|
-
return
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
discordLogger.log(`[BOT_SESSION] Detected bot-initiated thread: ${thread.name}`)
|
|
455
|
-
|
|
456
|
-
// Extract the prompt (everything after the prefix)
|
|
457
|
-
const prompt = starterMessage.content.slice(BOT_SESSION_PREFIX.length).trim()
|
|
465
|
+
const prompt = starterMessage.content.trim()
|
|
458
466
|
if (!prompt) {
|
|
459
467
|
discordLogger.log(`[BOT_SESSION] No prompt found in starter message`)
|
|
460
468
|
return
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// TaggedError definitions for type-safe error handling with errore.
|
|
2
|
+
// Errors are grouped by category: infrastructure, domain, and validation.
|
|
3
|
+
// Use errore.matchError() for exhaustive error handling in command handlers.
|
|
4
|
+
|
|
5
|
+
import * as errore from 'errore'
|
|
6
|
+
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
8
|
+
// INFRASTRUCTURE ERRORS - Server, filesystem, external services
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
+
|
|
11
|
+
export class DirectoryNotAccessibleError extends errore.TaggedError('DirectoryNotAccessibleError')<{
|
|
12
|
+
directory: string
|
|
13
|
+
message: string
|
|
14
|
+
}>() {
|
|
15
|
+
constructor(args: { directory: string }) {
|
|
16
|
+
super({ ...args, message: `Directory does not exist or is not accessible: ${args.directory}` })
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ServerStartError extends errore.TaggedError('ServerStartError')<{
|
|
21
|
+
port: number
|
|
22
|
+
reason: string
|
|
23
|
+
message: string
|
|
24
|
+
}>() {
|
|
25
|
+
constructor(args: { port: number; reason: string }) {
|
|
26
|
+
super({ ...args, message: `Server failed to start on port ${args.port}: ${args.reason}` })
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class ServerNotFoundError extends errore.TaggedError('ServerNotFoundError')<{
|
|
31
|
+
directory: string
|
|
32
|
+
message: string
|
|
33
|
+
}>() {
|
|
34
|
+
constructor(args: { directory: string }) {
|
|
35
|
+
super({ ...args, message: `OpenCode server not found for directory: ${args.directory}` })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class ServerNotReadyError extends errore.TaggedError('ServerNotReadyError')<{
|
|
40
|
+
directory: string
|
|
41
|
+
message: string
|
|
42
|
+
}>() {
|
|
43
|
+
constructor(args: { directory: string }) {
|
|
44
|
+
super({
|
|
45
|
+
...args,
|
|
46
|
+
message: `OpenCode server for directory "${args.directory}" is in an error state (no client available)`,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class ApiKeyMissingError extends errore.TaggedError('ApiKeyMissingError')<{
|
|
52
|
+
service: string
|
|
53
|
+
message: string
|
|
54
|
+
}>() {
|
|
55
|
+
constructor(args: { service: string }) {
|
|
56
|
+
super({ ...args, message: `${args.service} API key is required` })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
61
|
+
// DOMAIN ERRORS - Sessions, messages, transcription
|
|
62
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
63
|
+
|
|
64
|
+
export class SessionNotFoundError extends errore.TaggedError('SessionNotFoundError')<{
|
|
65
|
+
sessionId: string
|
|
66
|
+
message: string
|
|
67
|
+
}>() {
|
|
68
|
+
constructor(args: { sessionId: string }) {
|
|
69
|
+
super({ ...args, message: `Session ${args.sessionId} not found` })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class SessionCreateError extends errore.TaggedError('SessionCreateError')<{
|
|
74
|
+
message: string
|
|
75
|
+
cause?: unknown
|
|
76
|
+
}>() {}
|
|
77
|
+
|
|
78
|
+
export class MessagesNotFoundError extends errore.TaggedError('MessagesNotFoundError')<{
|
|
79
|
+
sessionId: string
|
|
80
|
+
message: string
|
|
81
|
+
}>() {
|
|
82
|
+
constructor(args: { sessionId: string }) {
|
|
83
|
+
super({ ...args, message: `No messages found for session ${args.sessionId}` })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class TranscriptionError extends errore.TaggedError('TranscriptionError')<{
|
|
88
|
+
reason: string
|
|
89
|
+
message: string
|
|
90
|
+
cause?: unknown
|
|
91
|
+
}>() {
|
|
92
|
+
constructor(args: { reason: string; cause?: unknown }) {
|
|
93
|
+
super({ ...args, message: `Transcription failed: ${args.reason}` })
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class GrepSearchError extends errore.TaggedError('GrepSearchError')<{
|
|
98
|
+
pattern: string
|
|
99
|
+
message: string
|
|
100
|
+
cause?: unknown
|
|
101
|
+
}>() {
|
|
102
|
+
constructor(args: { pattern: string; cause?: unknown }) {
|
|
103
|
+
super({ ...args, message: `Grep search failed for pattern: ${args.pattern}` })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class GlobSearchError extends errore.TaggedError('GlobSearchError')<{
|
|
108
|
+
pattern: string
|
|
109
|
+
message: string
|
|
110
|
+
cause?: unknown
|
|
111
|
+
}>() {
|
|
112
|
+
constructor(args: { pattern: string; cause?: unknown }) {
|
|
113
|
+
super({ ...args, message: `Glob search failed for pattern: ${args.pattern}` })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
118
|
+
// VALIDATION ERRORS - Input validation, format checks
|
|
119
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
120
|
+
|
|
121
|
+
export class InvalidAudioFormatError extends errore.TaggedError('InvalidAudioFormatError')<{
|
|
122
|
+
message: string
|
|
123
|
+
}>() {
|
|
124
|
+
constructor() {
|
|
125
|
+
super({ message: 'Invalid audio format' })
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export class EmptyTranscriptionError extends errore.TaggedError('EmptyTranscriptionError')<{
|
|
130
|
+
message: string
|
|
131
|
+
}>() {
|
|
132
|
+
constructor() {
|
|
133
|
+
super({ message: 'Model returned empty transcription' })
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class NoResponseContentError extends errore.TaggedError('NoResponseContentError')<{
|
|
138
|
+
message: string
|
|
139
|
+
}>() {
|
|
140
|
+
constructor() {
|
|
141
|
+
super({ message: 'No response content from model' })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export class NoToolResponseError extends errore.TaggedError('NoToolResponseError')<{
|
|
146
|
+
message: string
|
|
147
|
+
}>() {
|
|
148
|
+
constructor() {
|
|
149
|
+
super({ message: 'No valid tool responses' })
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
154
|
+
// NETWORK ERRORS - Fetch and HTTP
|
|
155
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
156
|
+
|
|
157
|
+
export class FetchError extends errore.TaggedError('FetchError')<{
|
|
158
|
+
url: string
|
|
159
|
+
message: string
|
|
160
|
+
cause?: unknown
|
|
161
|
+
}>() {
|
|
162
|
+
constructor(args: { url: string; cause?: unknown }) {
|
|
163
|
+
const causeMsg = args.cause instanceof Error ? args.cause.message : String(args.cause)
|
|
164
|
+
super({ ...args, message: `Fetch failed for ${args.url}: ${causeMsg}` })
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
169
|
+
// API ERRORS - External service responses
|
|
170
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
171
|
+
|
|
172
|
+
export class DiscordApiError extends errore.TaggedError('DiscordApiError')<{
|
|
173
|
+
status: number
|
|
174
|
+
message: string
|
|
175
|
+
}>() {
|
|
176
|
+
constructor(args: { status: number; body?: string }) {
|
|
177
|
+
super({ ...args, message: `Discord API error: ${args.status}${args.body ? ` - ${args.body}` : ''}` })
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export class OpenCodeApiError extends errore.TaggedError('OpenCodeApiError')<{
|
|
182
|
+
status: number
|
|
183
|
+
message: string
|
|
184
|
+
}>() {
|
|
185
|
+
constructor(args: { status: number; body?: string }) {
|
|
186
|
+
super({ ...args, message: `OpenCode API error (${args.status})${args.body ? `: ${args.body}` : ''}` })
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
191
|
+
// UNION TYPES - For function signatures
|
|
192
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
193
|
+
|
|
194
|
+
export type TranscriptionErrors =
|
|
195
|
+
| ApiKeyMissingError
|
|
196
|
+
| InvalidAudioFormatError
|
|
197
|
+
| TranscriptionError
|
|
198
|
+
| EmptyTranscriptionError
|
|
199
|
+
| NoResponseContentError
|
|
200
|
+
| NoToolResponseError
|
|
201
|
+
|
|
202
|
+
export type OpenCodeErrors =
|
|
203
|
+
| DirectoryNotAccessibleError
|
|
204
|
+
| ServerStartError
|
|
205
|
+
| ServerNotFoundError
|
|
206
|
+
| ServerNotReadyError
|
|
207
|
+
|
|
208
|
+
export type SessionErrors = SessionNotFoundError | MessagesNotFoundError | OpenCodeApiError
|
package/src/genai-worker.ts
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
import { parentPort, threadId } from 'node:worker_threads'
|
|
6
6
|
import { createWriteStream, type WriteStream } from 'node:fs'
|
|
7
|
-
import { mkdir } from 'node:fs/promises'
|
|
8
7
|
import path from 'node:path'
|
|
8
|
+
import * as errore from 'errore'
|
|
9
9
|
import { Resampler } from '@purinton/resampler'
|
|
10
10
|
import * as prism from 'prism-media'
|
|
11
11
|
import { startGenAiSession } from './genai.js'
|
|
12
12
|
import type { Session } from '@google/genai'
|
|
13
13
|
import { getTools } from './tools.js'
|
|
14
|
+
import { mkdir } from 'node:fs/promises'
|
|
14
15
|
import type { WorkerInMessage, WorkerOutMessage } from './worker-types.js'
|
|
15
16
|
import { createLogger } from './logger.js'
|
|
16
17
|
|
|
@@ -127,26 +128,28 @@ async function createAssistantAudioLogStream(
|
|
|
127
128
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
128
129
|
const audioDir = path.join(process.cwd(), 'discord-audio-logs', guildId, channelId)
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
const mkdirError = await errore.tryAsync({
|
|
132
|
+
try: () => mkdir(audioDir, { recursive: true }),
|
|
133
|
+
catch: (e) => e as Error,
|
|
134
|
+
})
|
|
135
|
+
if (errore.isError(mkdirError)) {
|
|
136
|
+
workerLogger.error(`Failed to create audio log directory:`, mkdirError.message)
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
// Create stream for assistant audio (24kHz mono s16le PCM)
|
|
141
|
+
const outputFileName = `assistant_${timestamp}.24.pcm`
|
|
142
|
+
const outputFilePath = path.join(audioDir, outputFileName)
|
|
143
|
+
const outputAudioStream = createWriteStream(outputFilePath)
|
|
137
144
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
// Add error handler to prevent crashes
|
|
146
|
+
outputAudioStream.on('error', (error) => {
|
|
147
|
+
workerLogger.error(`Assistant audio log stream error:`, error)
|
|
148
|
+
})
|
|
142
149
|
|
|
143
|
-
|
|
150
|
+
workerLogger.log(`Created assistant audio log: ${outputFilePath}`)
|
|
144
151
|
|
|
145
|
-
|
|
146
|
-
} catch (error) {
|
|
147
|
-
workerLogger.error(`Failed to create audio log directory:`, error)
|
|
148
|
-
return null
|
|
149
|
-
}
|
|
152
|
+
return outputAudioStream
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
// Handle encoded Opus packets
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
handleProviderSelectMenu,
|
|
21
21
|
handleModelSelectMenu,
|
|
22
22
|
} from './commands/model.js'
|
|
23
|
-
import { handleAgentCommand, handleAgentSelectMenu } from './commands/agent.js'
|
|
23
|
+
import { handleAgentCommand, handleAgentSelectMenu, handleQuickAgentCommand } from './commands/agent.js'
|
|
24
24
|
import { handleAskQuestionSelectMenu } from './commands/ask-question.js'
|
|
25
25
|
import { handleQueueCommand, handleClearQueueCommand } from './commands/queue.js'
|
|
26
26
|
import { handleUndoCommand, handleRedoCommand } from './commands/undo-redo.js'
|
|
@@ -136,6 +136,12 @@ export function registerInteractionHandler({
|
|
|
136
136
|
return
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// Handle quick agent commands (ending with -agent suffix, but not the base /agent command)
|
|
140
|
+
if (interaction.commandName.endsWith('-agent') && interaction.commandName !== 'agent') {
|
|
141
|
+
await handleQuickAgentCommand({ command: interaction, appId })
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
139
145
|
// Handle user-defined commands (ending with -cmd suffix)
|
|
140
146
|
if (interaction.commandName.endsWith('-cmd')) {
|
|
141
147
|
await handleUserCommand({ command: interaction, appId })
|
package/src/markdown.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { test, expect, beforeAll, afterAll } from 'vitest'
|
|
2
2
|
import { spawn, type ChildProcess } from 'child_process'
|
|
3
3
|
import { OpencodeClient } from '@opencode-ai/sdk'
|
|
4
|
+
import * as errore from 'errore'
|
|
4
5
|
import { ShareMarkdown, getCompactSessionContext } from './markdown.js'
|
|
5
6
|
|
|
6
7
|
let serverProcess: ChildProcess
|
|
@@ -121,11 +122,14 @@ test('generate markdown from first available session', async () => {
|
|
|
121
122
|
const exporter = new ShareMarkdown(client)
|
|
122
123
|
|
|
123
124
|
// Generate markdown with system info
|
|
124
|
-
const
|
|
125
|
+
const markdownResult = await exporter.generate({
|
|
125
126
|
sessionID,
|
|
126
127
|
includeSystemInfo: true,
|
|
127
128
|
})
|
|
128
129
|
|
|
130
|
+
expect(errore.isOk(markdownResult)).toBe(true)
|
|
131
|
+
const markdown = errore.unwrap(markdownResult)
|
|
132
|
+
|
|
129
133
|
console.log(`Generated markdown length: ${markdown.length} characters`)
|
|
130
134
|
|
|
131
135
|
// Basic assertions
|
|
@@ -299,13 +303,16 @@ test('generate markdown from multiple sessions', async () => {
|
|
|
299
303
|
test.skipIf(process.env.CI)('getCompactSessionContext generates compact format', async () => {
|
|
300
304
|
const sessionId = 'ses_46c2205e8ffeOll1JUSuYChSAM'
|
|
301
305
|
|
|
302
|
-
const
|
|
306
|
+
const contextResult = await getCompactSessionContext({
|
|
303
307
|
client,
|
|
304
308
|
sessionId,
|
|
305
309
|
includeSystemPrompt: true,
|
|
306
310
|
maxMessages: 15,
|
|
307
311
|
})
|
|
308
312
|
|
|
313
|
+
expect(errore.isOk(contextResult)).toBe(true)
|
|
314
|
+
const context = errore.unwrap(contextResult)
|
|
315
|
+
|
|
309
316
|
console.log(`Generated compact context length: ${context.length} characters`)
|
|
310
317
|
|
|
311
318
|
expect(context).toBeTruthy()
|
|
@@ -319,13 +326,16 @@ test.skipIf(process.env.CI)('getCompactSessionContext generates compact format',
|
|
|
319
326
|
test.skipIf(process.env.CI)('getCompactSessionContext without system prompt', async () => {
|
|
320
327
|
const sessionId = 'ses_46c2205e8ffeOll1JUSuYChSAM'
|
|
321
328
|
|
|
322
|
-
const
|
|
329
|
+
const contextResult = await getCompactSessionContext({
|
|
323
330
|
client,
|
|
324
331
|
sessionId,
|
|
325
332
|
includeSystemPrompt: false,
|
|
326
333
|
maxMessages: 10,
|
|
327
334
|
})
|
|
328
335
|
|
|
336
|
+
expect(errore.isOk(contextResult)).toBe(true)
|
|
337
|
+
const context = errore.unwrap(contextResult)
|
|
338
|
+
|
|
329
339
|
console.log(`Generated compact context (no system) length: ${context.length} characters`)
|
|
330
340
|
|
|
331
341
|
expect(context).toBeTruthy()
|