kimaki 0.4.38 → 0.4.40
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/cli.js +27 -23
- package/dist/commands/abort.js +15 -6
- package/dist/commands/add-project.js +9 -0
- package/dist/commands/agent.js +13 -1
- 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 +14 -1
- package/dist/commands/share.js +10 -1
- package/dist/commands/undo-redo.js +13 -4
- package/dist/commands/worktree.js +180 -0
- package/dist/database.js +57 -5
- package/dist/discord-bot.js +48 -10
- package/dist/discord-utils.js +36 -0
- package/dist/errors.js +109 -0
- package/dist/genai-worker.js +18 -16
- package/dist/interaction-handler.js +6 -2
- package/dist/markdown.js +100 -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 +100 -2
- 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 +4 -2
- package/src/cli.ts +31 -32
- package/src/commands/abort.ts +17 -7
- package/src/commands/add-project.ts +9 -0
- package/src/commands/agent.ts +13 -1
- 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 +14 -1
- package/src/commands/share.ts +11 -1
- package/src/commands/undo-redo.ts +15 -6
- package/src/commands/worktree.ts +243 -0
- package/src/database.ts +104 -4
- package/src/discord-bot.ts +49 -9
- package/src/discord-utils.ts +50 -0
- package/src/errors.ts +138 -0
- package/src/genai-worker.ts +20 -17
- package/src/interaction-handler.ts +7 -2
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +112 -95
- package/src/message-formatting.ts +55 -38
- package/src/opencode.ts +52 -49
- package/src/session-handler.ts +118 -3
- 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Bridges Discord messages to OpenCode sessions, manages voice connections,
|
|
3
3
|
// and orchestrates the main event loop for the Kimaki bot.
|
|
4
4
|
|
|
5
|
-
import { getDatabase, closeDatabase } from './database.js'
|
|
5
|
+
import { getDatabase, closeDatabase, getThreadWorktree } from './database.js'
|
|
6
6
|
import { initializeOpencodeForDirectory, getOpencodeServers } from './opencode.js'
|
|
7
7
|
import {
|
|
8
8
|
escapeBackticksInCodeBlocks,
|
|
@@ -53,11 +53,15 @@ 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'
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
// Increase connection pool to prevent deadlock when multiple sessions have open SSE streams.
|
|
62
|
+
// Each session's event.subscribe() holds a connection; without enough connections,
|
|
63
|
+
// regular HTTP requests (question.reply, session.prompt) get blocked → deadlock.
|
|
64
|
+
setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0, connections: 500 }))
|
|
61
65
|
|
|
62
66
|
const discordLogger = createLogger('DISCORD')
|
|
63
67
|
const voiceLogger = createLogger('VOICE')
|
|
@@ -149,10 +153,12 @@ export async function startDiscordBot({
|
|
|
149
153
|
}
|
|
150
154
|
if (message.partial) {
|
|
151
155
|
discordLogger.log(`Fetching partial message ${message.id}`)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
const fetched = await errore.tryAsync({
|
|
157
|
+
try: () => message.fetch(),
|
|
158
|
+
catch: (e) => e as Error,
|
|
159
|
+
})
|
|
160
|
+
if (fetched instanceof Error) {
|
|
161
|
+
discordLogger.log(`Failed to fetch partial message ${message.id}:`, fetched.message)
|
|
156
162
|
return
|
|
157
163
|
}
|
|
158
164
|
}
|
|
@@ -201,6 +207,29 @@ export async function startDiscordBot({
|
|
|
201
207
|
channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
202
208
|
}
|
|
203
209
|
|
|
210
|
+
// Check if this thread is a worktree thread
|
|
211
|
+
const worktreeInfo = getThreadWorktree(thread.id)
|
|
212
|
+
if (worktreeInfo) {
|
|
213
|
+
if (worktreeInfo.status === 'pending') {
|
|
214
|
+
await message.reply({
|
|
215
|
+
content: '⏳ Worktree is still being created. Please wait...',
|
|
216
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
217
|
+
})
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
if (worktreeInfo.status === 'error') {
|
|
221
|
+
await message.reply({
|
|
222
|
+
content: `❌ Worktree creation failed: ${worktreeInfo.error_message}`,
|
|
223
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
224
|
+
})
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
if (worktreeInfo.worktree_directory) {
|
|
228
|
+
projectDirectory = worktreeInfo.worktree_directory
|
|
229
|
+
discordLogger.log(`Using worktree directory: ${projectDirectory}`)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
204
233
|
if (channelAppId && channelAppId !== currentAppId) {
|
|
205
234
|
voiceLogger.log(
|
|
206
235
|
`[IGNORED] Thread belongs to different bot app (expected: ${currentAppId}, got: ${channelAppId})`,
|
|
@@ -256,30 +285,41 @@ export async function startDiscordBot({
|
|
|
256
285
|
if (projectDirectory) {
|
|
257
286
|
try {
|
|
258
287
|
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
288
|
+
if (getClient instanceof Error) {
|
|
289
|
+
voiceLogger.error(`[SESSION] Failed to initialize OpenCode client:`, getClient.message)
|
|
290
|
+
throw new Error(getClient.message)
|
|
291
|
+
}
|
|
259
292
|
const client = getClient()
|
|
260
293
|
|
|
261
294
|
// get current session context (without system prompt, it would be duplicated)
|
|
262
295
|
if (row.session_id) {
|
|
263
|
-
|
|
296
|
+
const result = await getCompactSessionContext({
|
|
264
297
|
client,
|
|
265
298
|
sessionId: row.session_id,
|
|
266
299
|
includeSystemPrompt: false,
|
|
267
300
|
maxMessages: 15,
|
|
268
301
|
})
|
|
302
|
+
if (errore.isOk(result)) {
|
|
303
|
+
currentSessionContext = result
|
|
304
|
+
}
|
|
269
305
|
}
|
|
270
306
|
|
|
271
307
|
// get last session context (with system prompt for project context)
|
|
272
|
-
const
|
|
308
|
+
const lastSessionResult = await getLastSessionId({
|
|
273
309
|
client,
|
|
274
310
|
excludeSessionId: row.session_id,
|
|
275
311
|
})
|
|
312
|
+
const lastSessionId = errore.unwrapOr(lastSessionResult, null)
|
|
276
313
|
if (lastSessionId) {
|
|
277
|
-
|
|
314
|
+
const result = await getCompactSessionContext({
|
|
278
315
|
client,
|
|
279
316
|
sessionId: lastSessionId,
|
|
280
317
|
includeSystemPrompt: true,
|
|
281
318
|
maxMessages: 10,
|
|
282
319
|
})
|
|
320
|
+
if (errore.isOk(result)) {
|
|
321
|
+
lastSessionContext = result
|
|
322
|
+
}
|
|
283
323
|
}
|
|
284
324
|
} catch (e) {
|
|
285
325
|
voiceLogger.error(`Could not get session context:`, e)
|
package/src/discord-utils.ts
CHANGED
|
@@ -9,6 +9,9 @@ import { formatMarkdownTables } from './format-tables.js'
|
|
|
9
9
|
import { limitHeadingDepth } from './limit-heading-depth.js'
|
|
10
10
|
import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js'
|
|
11
11
|
import { createLogger } from './logger.js'
|
|
12
|
+
import mime from 'mime'
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
import path from 'node:path'
|
|
12
15
|
|
|
13
16
|
const discordLogger = createLogger('DISCORD')
|
|
14
17
|
|
|
@@ -302,3 +305,50 @@ export function getKimakiMetadata(textChannel: TextChannel | null): {
|
|
|
302
305
|
|
|
303
306
|
return { projectDirectory, channelAppId }
|
|
304
307
|
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Upload files to a Discord thread/channel in a single message.
|
|
311
|
+
* Sending all files in one message causes Discord to display images in a grid layout.
|
|
312
|
+
*/
|
|
313
|
+
export async function uploadFilesToDiscord({
|
|
314
|
+
threadId,
|
|
315
|
+
botToken,
|
|
316
|
+
files,
|
|
317
|
+
}: {
|
|
318
|
+
threadId: string
|
|
319
|
+
botToken: string
|
|
320
|
+
files: string[]
|
|
321
|
+
}): Promise<void> {
|
|
322
|
+
if (files.length === 0) {
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Build attachments array for all files
|
|
327
|
+
const attachments = files.map((file, index) => ({
|
|
328
|
+
id: index,
|
|
329
|
+
filename: path.basename(file),
|
|
330
|
+
}))
|
|
331
|
+
|
|
332
|
+
const formData = new FormData()
|
|
333
|
+
formData.append('payload_json', JSON.stringify({ attachments }))
|
|
334
|
+
|
|
335
|
+
// Append each file with its array index, with correct MIME type for grid display
|
|
336
|
+
files.forEach((file, index) => {
|
|
337
|
+
const buffer = fs.readFileSync(file)
|
|
338
|
+
const mimeType = mime.getType(file) || 'application/octet-stream'
|
|
339
|
+
formData.append(`files[${index}]`, new Blob([buffer], { type: mimeType }), path.basename(file))
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
const response = await fetch(`https://discord.com/api/v10/channels/${threadId}/messages`, {
|
|
343
|
+
method: 'POST',
|
|
344
|
+
headers: {
|
|
345
|
+
Authorization: `Bot ${botToken}`,
|
|
346
|
+
},
|
|
347
|
+
body: formData,
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
if (!response.ok) {
|
|
351
|
+
const error = await response.text()
|
|
352
|
+
throw new Error(`Discord API error: ${response.status} - ${error}`)
|
|
353
|
+
}
|
|
354
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
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 { createTaggedError } from 'errore'
|
|
6
|
+
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
8
|
+
// INFRASTRUCTURE ERRORS - Server, filesystem, external services
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
+
|
|
11
|
+
export class DirectoryNotAccessibleError extends createTaggedError({
|
|
12
|
+
name: 'DirectoryNotAccessibleError',
|
|
13
|
+
message: 'Directory does not exist or is not accessible: $directory',
|
|
14
|
+
}) {}
|
|
15
|
+
|
|
16
|
+
export class ServerStartError extends createTaggedError({
|
|
17
|
+
name: 'ServerStartError',
|
|
18
|
+
message: 'Server failed to start on port $port: $reason',
|
|
19
|
+
}) {}
|
|
20
|
+
|
|
21
|
+
export class ServerNotFoundError extends createTaggedError({
|
|
22
|
+
name: 'ServerNotFoundError',
|
|
23
|
+
message: 'OpenCode server not found for directory: $directory',
|
|
24
|
+
}) {}
|
|
25
|
+
|
|
26
|
+
export class ServerNotReadyError extends createTaggedError({
|
|
27
|
+
name: 'ServerNotReadyError',
|
|
28
|
+
message: 'OpenCode server for directory "$directory" is in an error state (no client available)',
|
|
29
|
+
}) {}
|
|
30
|
+
|
|
31
|
+
export class ApiKeyMissingError extends createTaggedError({
|
|
32
|
+
name: 'ApiKeyMissingError',
|
|
33
|
+
message: '$service API key is required',
|
|
34
|
+
}) {}
|
|
35
|
+
|
|
36
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
37
|
+
// DOMAIN ERRORS - Sessions, messages, transcription
|
|
38
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
39
|
+
|
|
40
|
+
export class SessionNotFoundError extends createTaggedError({
|
|
41
|
+
name: 'SessionNotFoundError',
|
|
42
|
+
message: 'Session $sessionId not found',
|
|
43
|
+
}) {}
|
|
44
|
+
|
|
45
|
+
export class SessionCreateError extends createTaggedError({
|
|
46
|
+
name: 'SessionCreateError',
|
|
47
|
+
message: '$message',
|
|
48
|
+
}) {}
|
|
49
|
+
|
|
50
|
+
export class MessagesNotFoundError extends createTaggedError({
|
|
51
|
+
name: 'MessagesNotFoundError',
|
|
52
|
+
message: 'No messages found for session $sessionId',
|
|
53
|
+
}) {}
|
|
54
|
+
|
|
55
|
+
export class TranscriptionError extends createTaggedError({
|
|
56
|
+
name: 'TranscriptionError',
|
|
57
|
+
message: 'Transcription failed: $reason',
|
|
58
|
+
}) {}
|
|
59
|
+
|
|
60
|
+
export class GrepSearchError extends createTaggedError({
|
|
61
|
+
name: 'GrepSearchError',
|
|
62
|
+
message: 'Grep search failed for pattern: $pattern',
|
|
63
|
+
}) {}
|
|
64
|
+
|
|
65
|
+
export class GlobSearchError extends createTaggedError({
|
|
66
|
+
name: 'GlobSearchError',
|
|
67
|
+
message: 'Glob search failed for pattern: $pattern',
|
|
68
|
+
}) {}
|
|
69
|
+
|
|
70
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
71
|
+
// VALIDATION ERRORS - Input validation, format checks
|
|
72
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
73
|
+
|
|
74
|
+
export class InvalidAudioFormatError extends createTaggedError({
|
|
75
|
+
name: 'InvalidAudioFormatError',
|
|
76
|
+
message: 'Invalid audio format',
|
|
77
|
+
}) {}
|
|
78
|
+
|
|
79
|
+
export class EmptyTranscriptionError extends createTaggedError({
|
|
80
|
+
name: 'EmptyTranscriptionError',
|
|
81
|
+
message: 'Model returned empty transcription',
|
|
82
|
+
}) {}
|
|
83
|
+
|
|
84
|
+
export class NoResponseContentError extends createTaggedError({
|
|
85
|
+
name: 'NoResponseContentError',
|
|
86
|
+
message: 'No response content from model',
|
|
87
|
+
}) {}
|
|
88
|
+
|
|
89
|
+
export class NoToolResponseError extends createTaggedError({
|
|
90
|
+
name: 'NoToolResponseError',
|
|
91
|
+
message: 'No valid tool responses',
|
|
92
|
+
}) {}
|
|
93
|
+
|
|
94
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
95
|
+
// NETWORK ERRORS - Fetch and HTTP
|
|
96
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
97
|
+
|
|
98
|
+
export class FetchError extends createTaggedError({
|
|
99
|
+
name: 'FetchError',
|
|
100
|
+
message: 'Fetch failed for $url',
|
|
101
|
+
}) {}
|
|
102
|
+
|
|
103
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
104
|
+
// API ERRORS - External service responses
|
|
105
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
106
|
+
|
|
107
|
+
export class DiscordApiError extends createTaggedError({
|
|
108
|
+
name: 'DiscordApiError',
|
|
109
|
+
message: 'Discord API error: $status $body',
|
|
110
|
+
}) {}
|
|
111
|
+
|
|
112
|
+
export class OpenCodeApiError extends createTaggedError({
|
|
113
|
+
name: 'OpenCodeApiError',
|
|
114
|
+
message: 'OpenCode API error ($status): $body',
|
|
115
|
+
}) {}
|
|
116
|
+
|
|
117
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
118
|
+
// UNION TYPES - For function signatures
|
|
119
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
120
|
+
|
|
121
|
+
export type TranscriptionErrors =
|
|
122
|
+
| ApiKeyMissingError
|
|
123
|
+
| InvalidAudioFormatError
|
|
124
|
+
| TranscriptionError
|
|
125
|
+
| EmptyTranscriptionError
|
|
126
|
+
| NoResponseContentError
|
|
127
|
+
| NoToolResponseError
|
|
128
|
+
|
|
129
|
+
export type OpenCodeErrors =
|
|
130
|
+
| DirectoryNotAccessibleError
|
|
131
|
+
| ServerStartError
|
|
132
|
+
| ServerNotFoundError
|
|
133
|
+
| ServerNotReadyError
|
|
134
|
+
|
|
135
|
+
export type SessionErrors =
|
|
136
|
+
| SessionNotFoundError
|
|
137
|
+
| MessagesNotFoundError
|
|
138
|
+
| 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 (mkdirError instanceof Error) {
|
|
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
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { Events, type Client, type Interaction } from 'discord.js'
|
|
6
6
|
import { handleSessionCommand, handleSessionAutocomplete } from './commands/session.js'
|
|
7
|
+
import { handleNewWorktreeCommand } from './commands/worktree.js'
|
|
7
8
|
import { handleResumeCommand, handleResumeAutocomplete } from './commands/resume.js'
|
|
8
9
|
import { handleAddProjectCommand, handleAddProjectAutocomplete } from './commands/add-project.js'
|
|
9
10
|
import {
|
|
@@ -52,7 +53,7 @@ export function registerInteractionHandler({
|
|
|
52
53
|
|
|
53
54
|
if (interaction.isAutocomplete()) {
|
|
54
55
|
switch (interaction.commandName) {
|
|
55
|
-
case 'session':
|
|
56
|
+
case 'new-session':
|
|
56
57
|
await handleSessionAutocomplete({ interaction, appId })
|
|
57
58
|
return
|
|
58
59
|
|
|
@@ -78,10 +79,14 @@ export function registerInteractionHandler({
|
|
|
78
79
|
interactionLogger.log(`[COMMAND] Processing: ${interaction.commandName}`)
|
|
79
80
|
|
|
80
81
|
switch (interaction.commandName) {
|
|
81
|
-
case 'session':
|
|
82
|
+
case 'new-session':
|
|
82
83
|
await handleSessionCommand({ command: interaction, appId })
|
|
83
84
|
return
|
|
84
85
|
|
|
86
|
+
case 'new-worktree':
|
|
87
|
+
await handleNewWorktreeCommand({ command: interaction, appId })
|
|
88
|
+
return
|
|
89
|
+
|
|
85
90
|
case 'resume':
|
|
86
91
|
await handleResumeCommand({ command: interaction, appId })
|
|
87
92
|
return
|
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()
|