kimaki 0.4.34 → 0.4.36
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/ai-tool-to-genai.js +1 -3
- package/dist/channel-management.js +1 -1
- package/dist/cli.js +142 -39
- package/dist/commands/abort.js +1 -1
- package/dist/commands/add-project.js +1 -1
- package/dist/commands/agent.js +6 -2
- package/dist/commands/ask-question.js +2 -1
- package/dist/commands/fork.js +7 -7
- package/dist/commands/queue.js +2 -2
- package/dist/commands/remove-project.js +109 -0
- package/dist/commands/resume.js +3 -5
- package/dist/commands/session.js +56 -1
- package/dist/commands/share.js +1 -1
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/user-command.js +3 -6
- package/dist/config.js +1 -1
- package/dist/discord-bot.js +4 -10
- package/dist/discord-utils.js +33 -9
- package/dist/genai.js +4 -6
- package/dist/interaction-handler.js +8 -1
- package/dist/markdown.js +1 -3
- package/dist/message-formatting.js +7 -3
- package/dist/openai-realtime.js +3 -5
- package/dist/opencode.js +2 -3
- package/dist/session-handler.js +42 -25
- package/dist/system-message.js +5 -3
- package/dist/tools.js +9 -22
- package/dist/unnest-code-blocks.js +4 -2
- package/dist/unnest-code-blocks.test.js +40 -15
- package/dist/voice-handler.js +9 -12
- package/dist/voice.js +5 -3
- package/dist/xml.js +2 -4
- package/package.json +3 -2
- package/src/__snapshots__/compact-session-context-no-system.md +24 -24
- package/src/__snapshots__/compact-session-context.md +31 -31
- package/src/ai-tool-to-genai.ts +3 -11
- package/src/channel-management.ts +14 -25
- package/src/cli.ts +290 -195
- package/src/commands/abort.ts +1 -3
- package/src/commands/add-project.ts +8 -14
- package/src/commands/agent.ts +16 -9
- package/src/commands/ask-question.ts +8 -7
- package/src/commands/create-new-project.ts +8 -14
- package/src/commands/fork.ts +23 -27
- package/src/commands/model.ts +14 -11
- package/src/commands/permissions.ts +1 -1
- package/src/commands/queue.ts +6 -19
- package/src/commands/remove-project.ts +136 -0
- package/src/commands/resume.ts +11 -30
- package/src/commands/session.ts +68 -9
- package/src/commands/share.ts +1 -3
- package/src/commands/types.ts +1 -3
- package/src/commands/undo-redo.ts +6 -18
- package/src/commands/user-command.ts +8 -10
- package/src/config.ts +5 -5
- package/src/database.ts +10 -8
- package/src/discord-bot.ts +22 -46
- package/src/discord-utils.ts +35 -18
- package/src/escape-backticks.test.ts +0 -2
- package/src/format-tables.ts +1 -4
- package/src/genai-worker-wrapper.ts +3 -9
- package/src/genai-worker.ts +4 -19
- package/src/genai.ts +10 -42
- package/src/interaction-handler.ts +133 -121
- package/src/markdown.test.ts +10 -32
- package/src/markdown.ts +6 -14
- package/src/message-formatting.ts +13 -14
- package/src/openai-realtime.ts +25 -47
- package/src/opencode.ts +26 -37
- package/src/session-handler.ts +111 -75
- package/src/system-message.ts +13 -3
- package/src/tools.ts +13 -39
- package/src/unnest-code-blocks.test.ts +42 -15
- package/src/unnest-code-blocks.ts +4 -2
- package/src/utils.ts +1 -4
- package/src/voice-handler.ts +34 -78
- package/src/voice.ts +11 -19
- package/src/xml.test.ts +1 -1
- package/src/xml.ts +3 -12
package/src/genai.ts
CHANGED
|
@@ -2,13 +2,7 @@
|
|
|
2
2
|
// Establishes bidirectional audio streaming with Gemini, handles tool calls,
|
|
3
3
|
// and manages the assistant's audio output for Discord voice channels.
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
GoogleGenAI,
|
|
7
|
-
LiveServerMessage,
|
|
8
|
-
MediaResolution,
|
|
9
|
-
Modality,
|
|
10
|
-
Session,
|
|
11
|
-
} from '@google/genai'
|
|
5
|
+
import { GoogleGenAI, LiveServerMessage, MediaResolution, Modality, Session } from '@google/genai'
|
|
12
6
|
import type { CallableTool } from '@google/genai'
|
|
13
7
|
import { writeFile } from 'fs'
|
|
14
8
|
import type { Tool as AITool } from 'ai'
|
|
@@ -97,13 +91,7 @@ function createWavHeader(dataLength: number, options: WavConversionOptions) {
|
|
|
97
91
|
return buffer
|
|
98
92
|
}
|
|
99
93
|
|
|
100
|
-
function defaultAudioChunkHandler({
|
|
101
|
-
data,
|
|
102
|
-
mimeType,
|
|
103
|
-
}: {
|
|
104
|
-
data: Buffer
|
|
105
|
-
mimeType: string
|
|
106
|
-
}) {
|
|
94
|
+
function defaultAudioChunkHandler({ data, mimeType }: { data: Buffer; mimeType: string }) {
|
|
107
95
|
audioParts.push(data)
|
|
108
96
|
const fileName = 'audio.wav'
|
|
109
97
|
const buffer = convertToWav(audioParts, mimeType)
|
|
@@ -147,9 +135,7 @@ export async function startGenAiSession({
|
|
|
147
135
|
// Handle tool calls
|
|
148
136
|
if (message.toolCall.functionCalls && callableTools.length > 0) {
|
|
149
137
|
for (const tool of callableTools) {
|
|
150
|
-
if (
|
|
151
|
-
!message.toolCall.functionCalls.some((x) => x.name === tool.name)
|
|
152
|
-
) {
|
|
138
|
+
if (!message.toolCall.functionCalls.some((x) => x.name === tool.name)) {
|
|
153
139
|
continue
|
|
154
140
|
}
|
|
155
141
|
tool
|
|
@@ -158,20 +144,14 @@ export async function startGenAiSession({
|
|
|
158
144
|
const functionResponses = parts
|
|
159
145
|
.filter((part) => part.functionResponse)
|
|
160
146
|
.map((part) => ({
|
|
161
|
-
response: part.functionResponse!.response as Record<
|
|
162
|
-
string,
|
|
163
|
-
unknown
|
|
164
|
-
>,
|
|
147
|
+
response: part.functionResponse!.response as Record<string, unknown>,
|
|
165
148
|
id: part.functionResponse!.id,
|
|
166
149
|
name: part.functionResponse!.name,
|
|
167
150
|
}))
|
|
168
151
|
|
|
169
152
|
if (functionResponses.length > 0 && session) {
|
|
170
153
|
session.sendToolResponse({ functionResponses })
|
|
171
|
-
genaiLogger.log(
|
|
172
|
-
'client-toolResponse: ' +
|
|
173
|
-
JSON.stringify({ functionResponses }),
|
|
174
|
-
)
|
|
154
|
+
genaiLogger.log('client-toolResponse: ' + JSON.stringify({ functionResponses }))
|
|
175
155
|
}
|
|
176
156
|
})
|
|
177
157
|
.catch((error) => {
|
|
@@ -188,14 +168,8 @@ export async function startGenAiSession({
|
|
|
188
168
|
|
|
189
169
|
if (part?.inlineData) {
|
|
190
170
|
const inlineData = part.inlineData
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
!inlineData.mimeType.startsWith('audio/')
|
|
194
|
-
) {
|
|
195
|
-
genaiLogger.log(
|
|
196
|
-
'Skipping non-audio inlineData:',
|
|
197
|
-
inlineData.mimeType,
|
|
198
|
-
)
|
|
171
|
+
if (!inlineData.mimeType || !inlineData.mimeType.startsWith('audio/')) {
|
|
172
|
+
genaiLogger.log('Skipping non-audio inlineData:', inlineData.mimeType)
|
|
199
173
|
continue
|
|
200
174
|
}
|
|
201
175
|
|
|
@@ -219,18 +193,12 @@ export async function startGenAiSession({
|
|
|
219
193
|
}
|
|
220
194
|
// Handle input transcription (user's audio transcription)
|
|
221
195
|
if (message.serverContent?.inputTranscription?.text) {
|
|
222
|
-
genaiLogger.log(
|
|
223
|
-
'[user transcription]',
|
|
224
|
-
message.serverContent.inputTranscription.text,
|
|
225
|
-
)
|
|
196
|
+
genaiLogger.log('[user transcription]', message.serverContent.inputTranscription.text)
|
|
226
197
|
}
|
|
227
198
|
|
|
228
199
|
// Handle output transcription (model's audio transcription)
|
|
229
200
|
if (message.serverContent?.outputTranscription?.text) {
|
|
230
|
-
genaiLogger.log(
|
|
231
|
-
'[assistant transcription]',
|
|
232
|
-
message.serverContent.outputTranscription.text,
|
|
233
|
-
)
|
|
201
|
+
genaiLogger.log('[assistant transcription]', message.serverContent.outputTranscription.text)
|
|
234
202
|
}
|
|
235
203
|
if (message.serverContent?.interrupted) {
|
|
236
204
|
genaiLogger.log('Assistant was interrupted')
|
|
@@ -249,7 +217,7 @@ export async function startGenAiSession({
|
|
|
249
217
|
}
|
|
250
218
|
|
|
251
219
|
const apiKey = geminiApiKey || process.env.GEMINI_API_KEY
|
|
252
|
-
|
|
220
|
+
|
|
253
221
|
if (!apiKey) {
|
|
254
222
|
genaiLogger.error('No Gemini API key provided')
|
|
255
223
|
throw new Error('Gemini API key is required for voice interactions')
|
|
@@ -6,12 +6,20 @@ import { Events, type Client, type Interaction } from 'discord.js'
|
|
|
6
6
|
import { handleSessionCommand, handleSessionAutocomplete } from './commands/session.js'
|
|
7
7
|
import { handleResumeCommand, handleResumeAutocomplete } from './commands/resume.js'
|
|
8
8
|
import { handleAddProjectCommand, handleAddProjectAutocomplete } from './commands/add-project.js'
|
|
9
|
+
import {
|
|
10
|
+
handleRemoveProjectCommand,
|
|
11
|
+
handleRemoveProjectAutocomplete,
|
|
12
|
+
} from './commands/remove-project.js'
|
|
9
13
|
import { handleCreateNewProjectCommand } from './commands/create-new-project.js'
|
|
10
14
|
import { handlePermissionSelectMenu } from './commands/permissions.js'
|
|
11
15
|
import { handleAbortCommand } from './commands/abort.js'
|
|
12
16
|
import { handleShareCommand } from './commands/share.js'
|
|
13
17
|
import { handleForkCommand, handleForkSelectMenu } from './commands/fork.js'
|
|
14
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
handleModelCommand,
|
|
20
|
+
handleProviderSelectMenu,
|
|
21
|
+
handleModelSelectMenu,
|
|
22
|
+
} from './commands/model.js'
|
|
15
23
|
import { handleAgentCommand, handleAgentSelectMenu } from './commands/agent.js'
|
|
16
24
|
import { handleAskQuestionSelectMenu } from './commands/ask-question.js'
|
|
17
25
|
import { handleQueueCommand, handleClearQueueCommand } from './commands/queue.js'
|
|
@@ -21,7 +29,6 @@ import { createLogger } from './logger.js'
|
|
|
21
29
|
|
|
22
30
|
const interactionLogger = createLogger('INTERACTION')
|
|
23
31
|
|
|
24
|
-
|
|
25
32
|
export function registerInteractionHandler({
|
|
26
33
|
discordClient,
|
|
27
34
|
appId,
|
|
@@ -31,153 +38,158 @@ export function registerInteractionHandler({
|
|
|
31
38
|
}) {
|
|
32
39
|
interactionLogger.log('[REGISTER] Interaction handler registered')
|
|
33
40
|
|
|
34
|
-
discordClient.on(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
interaction.
|
|
41
|
-
? interaction.commandName
|
|
42
|
-
:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
await handleSessionAutocomplete({ interaction, appId })
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
case 'resume':
|
|
55
|
-
await handleResumeAutocomplete({ interaction, appId })
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
case 'add-project':
|
|
59
|
-
await handleAddProjectAutocomplete({ interaction, appId })
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
default:
|
|
63
|
-
await interaction.respond([])
|
|
64
|
-
return
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (interaction.isChatInputCommand()) {
|
|
69
|
-
interactionLogger.log(`[COMMAND] Processing: ${interaction.commandName}`)
|
|
70
|
-
|
|
71
|
-
switch (interaction.commandName) {
|
|
72
|
-
case 'session':
|
|
73
|
-
await handleSessionCommand({ command: interaction, appId })
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
case 'resume':
|
|
77
|
-
await handleResumeCommand({ command: interaction, appId })
|
|
78
|
-
return
|
|
41
|
+
discordClient.on(Events.InteractionCreate, async (interaction: Interaction) => {
|
|
42
|
+
try {
|
|
43
|
+
interactionLogger.log(
|
|
44
|
+
`[INTERACTION] Received: ${interaction.type} - ${
|
|
45
|
+
interaction.isChatInputCommand()
|
|
46
|
+
? interaction.commandName
|
|
47
|
+
: interaction.isAutocomplete()
|
|
48
|
+
? `autocomplete:${interaction.commandName}`
|
|
49
|
+
: 'other'
|
|
50
|
+
}`,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if (interaction.isAutocomplete()) {
|
|
54
|
+
switch (interaction.commandName) {
|
|
55
|
+
case 'session':
|
|
56
|
+
await handleSessionAutocomplete({ interaction, appId })
|
|
57
|
+
return
|
|
79
58
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
59
|
+
case 'resume':
|
|
60
|
+
await handleResumeAutocomplete({ interaction, appId })
|
|
61
|
+
return
|
|
83
62
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
63
|
+
case 'add-project':
|
|
64
|
+
await handleAddProjectAutocomplete({ interaction, appId })
|
|
65
|
+
return
|
|
87
66
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return
|
|
67
|
+
case 'remove-project':
|
|
68
|
+
await handleRemoveProjectAutocomplete({ interaction, appId })
|
|
69
|
+
return
|
|
92
70
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
71
|
+
default:
|
|
72
|
+
await interaction.respond([])
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
}
|
|
96
76
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return
|
|
77
|
+
if (interaction.isChatInputCommand()) {
|
|
78
|
+
interactionLogger.log(`[COMMAND] Processing: ${interaction.commandName}`)
|
|
100
79
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
80
|
+
switch (interaction.commandName) {
|
|
81
|
+
case 'session':
|
|
82
|
+
await handleSessionCommand({ command: interaction, appId })
|
|
83
|
+
return
|
|
104
84
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
85
|
+
case 'resume':
|
|
86
|
+
await handleResumeCommand({ command: interaction, appId })
|
|
87
|
+
return
|
|
108
88
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
89
|
+
case 'add-project':
|
|
90
|
+
await handleAddProjectCommand({ command: interaction, appId })
|
|
91
|
+
return
|
|
112
92
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
93
|
+
case 'remove-project':
|
|
94
|
+
await handleRemoveProjectCommand({ command: interaction, appId })
|
|
95
|
+
return
|
|
116
96
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
97
|
+
case 'create-new-project':
|
|
98
|
+
await handleCreateNewProjectCommand({ command: interaction, appId })
|
|
99
|
+
return
|
|
120
100
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
101
|
+
case 'abort':
|
|
102
|
+
case 'stop':
|
|
103
|
+
await handleAbortCommand({ command: interaction, appId })
|
|
104
|
+
return
|
|
125
105
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
await handleUserCommand({ command: interaction, appId })
|
|
106
|
+
case 'share':
|
|
107
|
+
await handleShareCommand({ command: interaction, appId })
|
|
129
108
|
return
|
|
130
|
-
}
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
109
|
|
|
134
|
-
|
|
135
|
-
|
|
110
|
+
case 'fork':
|
|
111
|
+
await handleForkCommand(interaction)
|
|
112
|
+
return
|
|
136
113
|
|
|
137
|
-
|
|
138
|
-
await
|
|
114
|
+
case 'model':
|
|
115
|
+
await handleModelCommand({ interaction, appId })
|
|
139
116
|
return
|
|
140
|
-
}
|
|
141
117
|
|
|
142
|
-
|
|
143
|
-
await
|
|
118
|
+
case 'agent':
|
|
119
|
+
await handleAgentCommand({ interaction, appId })
|
|
144
120
|
return
|
|
145
|
-
}
|
|
146
121
|
|
|
147
|
-
|
|
148
|
-
await
|
|
122
|
+
case 'queue':
|
|
123
|
+
await handleQueueCommand({ command: interaction, appId })
|
|
149
124
|
return
|
|
150
|
-
}
|
|
151
125
|
|
|
152
|
-
|
|
153
|
-
await
|
|
126
|
+
case 'clear-queue':
|
|
127
|
+
await handleClearQueueCommand({ command: interaction, appId })
|
|
154
128
|
return
|
|
155
|
-
}
|
|
156
129
|
|
|
157
|
-
|
|
158
|
-
await
|
|
130
|
+
case 'undo':
|
|
131
|
+
await handleUndoCommand({ command: interaction, appId })
|
|
159
132
|
return
|
|
160
|
-
}
|
|
161
133
|
|
|
162
|
-
|
|
163
|
-
await
|
|
134
|
+
case 'redo':
|
|
135
|
+
await handleRedoCommand({ command: interaction, appId })
|
|
164
136
|
return
|
|
165
|
-
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Handle user-defined commands (ending with -cmd suffix)
|
|
140
|
+
if (interaction.commandName.endsWith('-cmd')) {
|
|
141
|
+
await handleUserCommand({ command: interaction, appId })
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (interaction.isStringSelectMenu()) {
|
|
148
|
+
const customId = interaction.customId
|
|
149
|
+
|
|
150
|
+
if (customId.startsWith('fork_select:')) {
|
|
151
|
+
await handleForkSelectMenu(interaction)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (customId.startsWith('model_provider:')) {
|
|
156
|
+
await handleProviderSelectMenu(interaction)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (customId.startsWith('model_select:')) {
|
|
161
|
+
await handleModelSelectMenu(interaction)
|
|
166
162
|
return
|
|
167
163
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
164
|
+
|
|
165
|
+
if (customId.startsWith('agent_select:')) {
|
|
166
|
+
await handleAgentSelectMenu(interaction)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (customId.startsWith('ask_question:')) {
|
|
171
|
+
await handleAskQuestionSelectMenu(interaction)
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (customId.startsWith('permission:')) {
|
|
176
|
+
await handlePermissionSelectMenu(interaction)
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
interactionLogger.error('[INTERACTION] Error handling interaction:', error)
|
|
183
|
+
try {
|
|
184
|
+
if (interaction.isRepliable() && !interaction.replied && !interaction.deferred) {
|
|
185
|
+
await interaction.reply({
|
|
186
|
+
content: 'An error occurred processing this command.',
|
|
187
|
+
ephemeral: true,
|
|
188
|
+
})
|
|
179
189
|
}
|
|
190
|
+
} catch (replyError) {
|
|
191
|
+
interactionLogger.error('[INTERACTION] Failed to send error reply:', replyError)
|
|
180
192
|
}
|
|
181
|
-
}
|
|
182
|
-
)
|
|
193
|
+
}
|
|
194
|
+
})
|
|
183
195
|
}
|
package/src/markdown.test.ts
CHANGED
|
@@ -35,9 +35,7 @@ const waitForServer = async (port: number, maxAttempts = 30) => {
|
|
|
35
35
|
console.log(`Waiting for server... attempt ${i + 1}/${maxAttempts}`)
|
|
36
36
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
37
37
|
}
|
|
38
|
-
throw new Error(
|
|
39
|
-
`Server did not start on port ${port} after ${maxAttempts} seconds`,
|
|
40
|
-
)
|
|
38
|
+
throw new Error(`Server did not start on port ${port} after ${maxAttempts} seconds`)
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
beforeAll(async () => {
|
|
@@ -117,9 +115,7 @@ test('generate markdown from first available session', async () => {
|
|
|
117
115
|
// Take the first kimaki session
|
|
118
116
|
const firstSession = kimakiSessions[0]
|
|
119
117
|
const sessionID = firstSession!.id
|
|
120
|
-
console.log(
|
|
121
|
-
`Using session ID: ${sessionID} (${firstSession!.title || 'Untitled'})`,
|
|
122
|
-
)
|
|
118
|
+
console.log(`Using session ID: ${sessionID} (${firstSession!.title || 'Untitled'})`)
|
|
123
119
|
|
|
124
120
|
// Create markdown exporter
|
|
125
121
|
const exporter = new ShareMarkdown(client)
|
|
@@ -139,9 +135,7 @@ test('generate markdown from first available session', async () => {
|
|
|
139
135
|
expect(markdown).toContain('## Conversation')
|
|
140
136
|
|
|
141
137
|
// Save snapshot to file
|
|
142
|
-
await expect(markdown).toMatchFileSnapshot(
|
|
143
|
-
'./__snapshots__/first-session-with-info.md',
|
|
144
|
-
)
|
|
138
|
+
await expect(markdown).toMatchFileSnapshot('./__snapshots__/first-session-with-info.md')
|
|
145
139
|
})
|
|
146
140
|
|
|
147
141
|
test('generate markdown without system info', async () => {
|
|
@@ -184,9 +178,7 @@ test('generate markdown without system info', async () => {
|
|
|
184
178
|
expect(markdown).toContain('## Conversation')
|
|
185
179
|
|
|
186
180
|
// Save snapshot to file
|
|
187
|
-
await expect(markdown).toMatchFileSnapshot(
|
|
188
|
-
'./__snapshots__/first-session-no-info.md',
|
|
189
|
-
)
|
|
181
|
+
await expect(markdown).toMatchFileSnapshot('./__snapshots__/first-session-no-info.md')
|
|
190
182
|
})
|
|
191
183
|
|
|
192
184
|
test('generate markdown from session with tools', async () => {
|
|
@@ -218,11 +210,7 @@ test('generate markdown from session with tools', async () => {
|
|
|
218
210
|
const messages = await client.session.messages({
|
|
219
211
|
path: { id: session.id },
|
|
220
212
|
})
|
|
221
|
-
if (
|
|
222
|
-
messages.data?.some((msg) =>
|
|
223
|
-
msg.parts?.some((part) => part.type === 'tool'),
|
|
224
|
-
)
|
|
225
|
-
) {
|
|
213
|
+
if (messages.data?.some((msg) => msg.parts?.some((part) => part.type === 'tool'))) {
|
|
226
214
|
sessionWithTools = session
|
|
227
215
|
console.log(`Found session with tools: ${session.id}`)
|
|
228
216
|
break
|
|
@@ -233,9 +221,7 @@ test('generate markdown from session with tools', async () => {
|
|
|
233
221
|
}
|
|
234
222
|
|
|
235
223
|
if (!sessionWithTools) {
|
|
236
|
-
console.warn(
|
|
237
|
-
'No kimaki session with tool usage found, using first kimaki session',
|
|
238
|
-
)
|
|
224
|
+
console.warn('No kimaki session with tool usage found, using first kimaki session')
|
|
239
225
|
sessionWithTools = kimakiSessions[0]
|
|
240
226
|
}
|
|
241
227
|
|
|
@@ -245,9 +231,7 @@ test('generate markdown from session with tools', async () => {
|
|
|
245
231
|
})
|
|
246
232
|
|
|
247
233
|
expect(markdown).toBeTruthy()
|
|
248
|
-
await expect(markdown).toMatchFileSnapshot(
|
|
249
|
-
'./__snapshots__/session-with-tools.md',
|
|
250
|
-
)
|
|
234
|
+
await expect(markdown).toMatchFileSnapshot('./__snapshots__/session-with-tools.md')
|
|
251
235
|
})
|
|
252
236
|
|
|
253
237
|
test('error handling for non-existent session', async () => {
|
|
@@ -303,9 +287,7 @@ test('generate markdown from multiple sessions', async () => {
|
|
|
303
287
|
})
|
|
304
288
|
|
|
305
289
|
expect(markdown).toBeTruthy()
|
|
306
|
-
await expect(markdown).toMatchFileSnapshot(
|
|
307
|
-
`./__snapshots__/session-${i + 1}.md`,
|
|
308
|
-
)
|
|
290
|
+
await expect(markdown).toMatchFileSnapshot(`./__snapshots__/session-${i + 1}.md`)
|
|
309
291
|
} catch (e) {
|
|
310
292
|
console.error(`Error generating markdown for session ${session!.id}:`, e)
|
|
311
293
|
// Continue with other sessions
|
|
@@ -331,9 +313,7 @@ test.skipIf(process.env.CI)('getCompactSessionContext generates compact format',
|
|
|
331
313
|
// should have tool calls or messages
|
|
332
314
|
expect(context).toMatch(/\[Tool \w+\]:|\[User\]:|\[Assistant\]:/)
|
|
333
315
|
|
|
334
|
-
await expect(context).toMatchFileSnapshot(
|
|
335
|
-
'./__snapshots__/compact-session-context.md',
|
|
336
|
-
)
|
|
316
|
+
await expect(context).toMatchFileSnapshot('./__snapshots__/compact-session-context.md')
|
|
337
317
|
})
|
|
338
318
|
|
|
339
319
|
test.skipIf(process.env.CI)('getCompactSessionContext without system prompt', async () => {
|
|
@@ -352,7 +332,5 @@ test.skipIf(process.env.CI)('getCompactSessionContext without system prompt', as
|
|
|
352
332
|
// should NOT have system prompt
|
|
353
333
|
expect(context).not.toContain('[System Prompt]')
|
|
354
334
|
|
|
355
|
-
await expect(context).toMatchFileSnapshot(
|
|
356
|
-
'./__snapshots__/compact-session-context-no-system.md',
|
|
357
|
-
)
|
|
335
|
+
await expect(context).toMatchFileSnapshot('./__snapshots__/compact-session-context-no-system.md')
|
|
358
336
|
})
|
package/src/markdown.ts
CHANGED
|
@@ -46,9 +46,7 @@ export class ShareMarkdown {
|
|
|
46
46
|
// If lastAssistantOnly, filter to only the last assistant message
|
|
47
47
|
const messagesToRender = lastAssistantOnly
|
|
48
48
|
? (() => {
|
|
49
|
-
const assistantMessages = messages.filter(
|
|
50
|
-
(m) => m.info.role === 'assistant',
|
|
51
|
-
)
|
|
49
|
+
const assistantMessages = messages.filter((m) => m.info.role === 'assistant')
|
|
52
50
|
return assistantMessages.length > 0
|
|
53
51
|
? [assistantMessages[assistantMessages.length - 1]]
|
|
54
52
|
: []
|
|
@@ -68,12 +66,8 @@ export class ShareMarkdown {
|
|
|
68
66
|
if (includeSystemInfo === true) {
|
|
69
67
|
lines.push('## Session Information')
|
|
70
68
|
lines.push('')
|
|
71
|
-
lines.push(
|
|
72
|
-
|
|
73
|
-
)
|
|
74
|
-
lines.push(
|
|
75
|
-
`- **Updated**: ${formatDateTime(new Date(session.time.updated))}`,
|
|
76
|
-
)
|
|
69
|
+
lines.push(`- **Created**: ${formatDateTime(new Date(session.time.created))}`)
|
|
70
|
+
lines.push(`- **Updated**: ${formatDateTime(new Date(session.time.updated))}`)
|
|
77
71
|
if (session.version) {
|
|
78
72
|
lines.push(`- **OpenCode Version**: v${session.version}`)
|
|
79
73
|
}
|
|
@@ -308,10 +302,7 @@ export async function getCompactSessionContext({
|
|
|
308
302
|
|
|
309
303
|
// Get tool calls in compact form (name + params only)
|
|
310
304
|
const toolParts = (msg.parts || []).filter(
|
|
311
|
-
(p) =>
|
|
312
|
-
p.type === 'tool' &&
|
|
313
|
-
'state' in p &&
|
|
314
|
-
p.state?.status === 'completed',
|
|
305
|
+
(p) => p.type === 'tool' && 'state' in p && p.state?.status === 'completed',
|
|
315
306
|
)
|
|
316
307
|
for (const part of toolParts) {
|
|
317
308
|
if (part.type === 'tool' && 'tool' in part && 'state' in part) {
|
|
@@ -324,7 +315,8 @@ export async function getCompactSessionContext({
|
|
|
324
315
|
// compact params: just key=value on one line
|
|
325
316
|
const params = Object.entries(input)
|
|
326
317
|
.map(([k, v]) => {
|
|
327
|
-
const val =
|
|
318
|
+
const val =
|
|
319
|
+
typeof v === 'string' ? v.slice(0, 100) : JSON.stringify(v).slice(0, 100)
|
|
328
320
|
return `${k}=${val}`
|
|
329
321
|
})
|
|
330
322
|
.join(', ')
|
|
@@ -77,8 +77,8 @@ export function isTextMimeType(contentType: string | null): boolean {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export async function getTextAttachments(message: Message): Promise<string> {
|
|
80
|
-
const textAttachments = Array.from(message.attachments.values()).filter(
|
|
81
|
-
|
|
80
|
+
const textAttachments = Array.from(message.attachments.values()).filter((attachment) =>
|
|
81
|
+
isTextMimeType(attachment.contentType),
|
|
82
82
|
)
|
|
83
83
|
|
|
84
84
|
if (textAttachments.length === 0) {
|
|
@@ -105,14 +105,10 @@ export async function getTextAttachments(message: Message): Promise<string> {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
export async function getFileAttachments(message: Message): Promise<FilePartInput[]> {
|
|
108
|
-
const fileAttachments = Array.from(message.attachments.values()).filter(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
contentType.startsWith('image/') || contentType === 'application/pdf'
|
|
113
|
-
)
|
|
114
|
-
},
|
|
115
|
-
)
|
|
108
|
+
const fileAttachments = Array.from(message.attachments.values()).filter((attachment) => {
|
|
109
|
+
const contentType = attachment.contentType || ''
|
|
110
|
+
return contentType.startsWith('image/') || contentType === 'application/pdf'
|
|
111
|
+
})
|
|
116
112
|
|
|
117
113
|
if (fileAttachments.length === 0) {
|
|
118
114
|
return []
|
|
@@ -164,7 +160,9 @@ export function getToolSummaryText(part: Part): string {
|
|
|
164
160
|
const added = newString.split('\n').length
|
|
165
161
|
const removed = oldString.split('\n').length
|
|
166
162
|
const fileName = filePath.split('/').pop() || ''
|
|
167
|
-
return fileName
|
|
163
|
+
return fileName
|
|
164
|
+
? `*${escapeInlineMarkdown(fileName)}* (+${added}-${removed})`
|
|
165
|
+
: `(+${added}-${removed})`
|
|
168
166
|
}
|
|
169
167
|
|
|
170
168
|
if (part.tool === 'write') {
|
|
@@ -172,7 +170,9 @@ export function getToolSummaryText(part: Part): string {
|
|
|
172
170
|
const content = (part.state.input?.content as string) || ''
|
|
173
171
|
const lines = content.split('\n').length
|
|
174
172
|
const fileName = filePath.split('/').pop() || ''
|
|
175
|
-
return fileName
|
|
173
|
+
return fileName
|
|
174
|
+
? `*${escapeInlineMarkdown(fileName)}* (${lines} line${lines === 1 ? '' : 's'})`
|
|
175
|
+
: `(${lines} line${lines === 1 ? '' : 's'})`
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
if (part.tool === 'webfetch') {
|
|
@@ -259,8 +259,7 @@ export function formatPart(part: Part): string {
|
|
|
259
259
|
const trimmed = part.text.trimStart()
|
|
260
260
|
const firstChar = trimmed[0] || ''
|
|
261
261
|
const markdownStarters = ['#', '*', '_', '-', '>', '`', '[', '|']
|
|
262
|
-
const startsWithMarkdown =
|
|
263
|
-
markdownStarters.includes(firstChar) || /^\d+\./.test(trimmed)
|
|
262
|
+
const startsWithMarkdown = markdownStarters.includes(firstChar) || /^\d+\./.test(trimmed)
|
|
264
263
|
if (startsWithMarkdown) {
|
|
265
264
|
return `\n${part.text}`
|
|
266
265
|
}
|