kimaki 0.4.35 → 0.4.37

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.
Files changed (76) hide show
  1. package/dist/ai-tool-to-genai.js +1 -3
  2. package/dist/channel-management.js +5 -5
  3. package/dist/cli.js +182 -46
  4. package/dist/commands/abort.js +1 -1
  5. package/dist/commands/add-project.js +1 -1
  6. package/dist/commands/agent.js +6 -2
  7. package/dist/commands/ask-question.js +2 -1
  8. package/dist/commands/fork.js +7 -7
  9. package/dist/commands/queue.js +2 -2
  10. package/dist/commands/remove-project.js +109 -0
  11. package/dist/commands/resume.js +3 -5
  12. package/dist/commands/session.js +2 -2
  13. package/dist/commands/share.js +1 -1
  14. package/dist/commands/undo-redo.js +2 -2
  15. package/dist/commands/user-command.js +3 -6
  16. package/dist/config.js +1 -1
  17. package/dist/database.js +7 -0
  18. package/dist/discord-bot.js +37 -20
  19. package/dist/discord-utils.js +33 -9
  20. package/dist/genai.js +4 -6
  21. package/dist/interaction-handler.js +8 -1
  22. package/dist/markdown.js +1 -3
  23. package/dist/message-formatting.js +7 -3
  24. package/dist/openai-realtime.js +3 -5
  25. package/dist/opencode.js +1 -1
  26. package/dist/session-handler.js +25 -15
  27. package/dist/system-message.js +10 -4
  28. package/dist/tools.js +9 -22
  29. package/dist/voice-handler.js +9 -12
  30. package/dist/voice.js +5 -3
  31. package/dist/xml.js +2 -4
  32. package/package.json +3 -2
  33. package/src/__snapshots__/compact-session-context-no-system.md +24 -24
  34. package/src/__snapshots__/compact-session-context.md +31 -31
  35. package/src/ai-tool-to-genai.ts +3 -11
  36. package/src/channel-management.ts +18 -29
  37. package/src/cli.ts +334 -205
  38. package/src/commands/abort.ts +1 -3
  39. package/src/commands/add-project.ts +8 -14
  40. package/src/commands/agent.ts +16 -9
  41. package/src/commands/ask-question.ts +8 -7
  42. package/src/commands/create-new-project.ts +8 -14
  43. package/src/commands/fork.ts +23 -27
  44. package/src/commands/model.ts +14 -11
  45. package/src/commands/permissions.ts +1 -1
  46. package/src/commands/queue.ts +6 -19
  47. package/src/commands/remove-project.ts +136 -0
  48. package/src/commands/resume.ts +11 -30
  49. package/src/commands/session.ts +4 -13
  50. package/src/commands/share.ts +1 -3
  51. package/src/commands/types.ts +1 -3
  52. package/src/commands/undo-redo.ts +6 -18
  53. package/src/commands/user-command.ts +8 -10
  54. package/src/config.ts +5 -5
  55. package/src/database.ts +17 -8
  56. package/src/discord-bot.ts +60 -58
  57. package/src/discord-utils.ts +35 -18
  58. package/src/escape-backticks.test.ts +0 -2
  59. package/src/format-tables.ts +1 -4
  60. package/src/genai-worker-wrapper.ts +3 -9
  61. package/src/genai-worker.ts +4 -19
  62. package/src/genai.ts +10 -42
  63. package/src/interaction-handler.ts +133 -121
  64. package/src/markdown.test.ts +10 -32
  65. package/src/markdown.ts +6 -14
  66. package/src/message-formatting.ts +13 -14
  67. package/src/openai-realtime.ts +25 -47
  68. package/src/opencode.ts +24 -34
  69. package/src/session-handler.ts +91 -61
  70. package/src/system-message.ts +18 -4
  71. package/src/tools.ts +13 -39
  72. package/src/utils.ts +1 -4
  73. package/src/voice-handler.ts +34 -78
  74. package/src/voice.ts +11 -19
  75. package/src/xml.test.ts +1 -1
  76. package/src/xml.ts +3 -12
@@ -40,12 +40,7 @@ process.on('uncaughtException', (error) => {
40
40
  })
41
41
 
42
42
  process.on('unhandledRejection', (reason, promise) => {
43
- workerLogger.error(
44
- 'Unhandled rejection in worker:',
45
- reason,
46
- 'at promise:',
47
- promise,
48
- )
43
+ workerLogger.error('Unhandled rejection in worker:', reason, 'at promise:', promise)
49
44
  sendError(`Worker unhandled rejection: ${reason}`)
50
45
  })
51
46
 
@@ -130,12 +125,7 @@ async function createAssistantAudioLogStream(
130
125
  if (!process.env.DEBUG) return null
131
126
 
132
127
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
133
- const audioDir = path.join(
134
- process.cwd(),
135
- 'discord-audio-logs',
136
- guildId,
137
- channelId,
138
- )
128
+ const audioDir = path.join(process.cwd(), 'discord-audio-logs', guildId, channelId)
139
129
 
140
130
  try {
141
131
  await mkdir(audioDir, { recursive: true })
@@ -252,10 +242,7 @@ parentPort.on('message', async (message: WorkerInMessage) => {
252
242
  workerLogger.log(`Initializing with directory:`, message.directory)
253
243
 
254
244
  // Create audio log stream for assistant audio
255
- audioLogStream = await createAssistantAudioLogStream(
256
- message.guildId,
257
- message.channelId,
258
- )
245
+ audioLogStream = await createAssistantAudioLogStream(message.guildId, message.channelId)
259
246
 
260
247
  // Start packet sending interval
261
248
  startPacketSending()
@@ -359,8 +346,6 @@ parentPort.on('message', async (message: WorkerInMessage) => {
359
346
  }
360
347
  } catch (error) {
361
348
  workerLogger.error(`Error handling message:`, error)
362
- sendError(
363
- error instanceof Error ? error.message : 'Unknown error in worker',
364
- )
349
+ sendError(error instanceof Error ? error.message : 'Unknown error in worker')
365
350
  }
366
351
  })
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
- !inlineData.mimeType ||
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 { handleModelCommand, handleProviderSelectMenu, handleModelSelectMenu } from './commands/model.js'
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
- Events.InteractionCreate,
36
- async (interaction: Interaction) => {
37
- try {
38
- interactionLogger.log(
39
- `[INTERACTION] Received: ${interaction.type} - ${
40
- interaction.isChatInputCommand()
41
- ? interaction.commandName
42
- : interaction.isAutocomplete()
43
- ? `autocomplete:${interaction.commandName}`
44
- : 'other'
45
- }`,
46
- )
47
-
48
- if (interaction.isAutocomplete()) {
49
- switch (interaction.commandName) {
50
- case 'session':
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
- case 'add-project':
81
- await handleAddProjectCommand({ command: interaction, appId })
82
- return
59
+ case 'resume':
60
+ await handleResumeAutocomplete({ interaction, appId })
61
+ return
83
62
 
84
- case 'create-new-project':
85
- await handleCreateNewProjectCommand({ command: interaction, appId })
86
- return
63
+ case 'add-project':
64
+ await handleAddProjectAutocomplete({ interaction, appId })
65
+ return
87
66
 
88
- case 'abort':
89
- case 'stop':
90
- await handleAbortCommand({ command: interaction, appId })
91
- return
67
+ case 'remove-project':
68
+ await handleRemoveProjectAutocomplete({ interaction, appId })
69
+ return
92
70
 
93
- case 'share':
94
- await handleShareCommand({ command: interaction, appId })
95
- return
71
+ default:
72
+ await interaction.respond([])
73
+ return
74
+ }
75
+ }
96
76
 
97
- case 'fork':
98
- await handleForkCommand(interaction)
99
- return
77
+ if (interaction.isChatInputCommand()) {
78
+ interactionLogger.log(`[COMMAND] Processing: ${interaction.commandName}`)
100
79
 
101
- case 'model':
102
- await handleModelCommand({ interaction, appId })
103
- return
80
+ switch (interaction.commandName) {
81
+ case 'session':
82
+ await handleSessionCommand({ command: interaction, appId })
83
+ return
104
84
 
105
- case 'agent':
106
- await handleAgentCommand({ interaction, appId })
107
- return
85
+ case 'resume':
86
+ await handleResumeCommand({ command: interaction, appId })
87
+ return
108
88
 
109
- case 'queue':
110
- await handleQueueCommand({ command: interaction, appId })
111
- return
89
+ case 'add-project':
90
+ await handleAddProjectCommand({ command: interaction, appId })
91
+ return
112
92
 
113
- case 'clear-queue':
114
- await handleClearQueueCommand({ command: interaction, appId })
115
- return
93
+ case 'remove-project':
94
+ await handleRemoveProjectCommand({ command: interaction, appId })
95
+ return
116
96
 
117
- case 'undo':
118
- await handleUndoCommand({ command: interaction, appId })
119
- return
97
+ case 'create-new-project':
98
+ await handleCreateNewProjectCommand({ command: interaction, appId })
99
+ return
120
100
 
121
- case 'redo':
122
- await handleRedoCommand({ command: interaction, appId })
123
- return
124
- }
101
+ case 'abort':
102
+ case 'stop':
103
+ await handleAbortCommand({ command: interaction, appId })
104
+ return
125
105
 
126
- // Handle user-defined commands (ending with -cmd suffix)
127
- if (interaction.commandName.endsWith('-cmd')) {
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
- if (interaction.isStringSelectMenu()) {
135
- const customId = interaction.customId
110
+ case 'fork':
111
+ await handleForkCommand(interaction)
112
+ return
136
113
 
137
- if (customId.startsWith('fork_select:')) {
138
- await handleForkSelectMenu(interaction)
114
+ case 'model':
115
+ await handleModelCommand({ interaction, appId })
139
116
  return
140
- }
141
117
 
142
- if (customId.startsWith('model_provider:')) {
143
- await handleProviderSelectMenu(interaction)
118
+ case 'agent':
119
+ await handleAgentCommand({ interaction, appId })
144
120
  return
145
- }
146
121
 
147
- if (customId.startsWith('model_select:')) {
148
- await handleModelSelectMenu(interaction)
122
+ case 'queue':
123
+ await handleQueueCommand({ command: interaction, appId })
149
124
  return
150
- }
151
125
 
152
- if (customId.startsWith('agent_select:')) {
153
- await handleAgentSelectMenu(interaction)
126
+ case 'clear-queue':
127
+ await handleClearQueueCommand({ command: interaction, appId })
154
128
  return
155
- }
156
129
 
157
- if (customId.startsWith('ask_question:')) {
158
- await handleAskQuestionSelectMenu(interaction)
130
+ case 'undo':
131
+ await handleUndoCommand({ command: interaction, appId })
159
132
  return
160
- }
161
133
 
162
- if (customId.startsWith('permission:')) {
163
- await handlePermissionSelectMenu(interaction)
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
- } catch (error) {
169
- interactionLogger.error('[INTERACTION] Error handling interaction:', error)
170
- try {
171
- if (interaction.isRepliable() && !interaction.replied && !interaction.deferred) {
172
- await interaction.reply({
173
- content: 'An error occurred processing this command.',
174
- ephemeral: true,
175
- })
176
- }
177
- } catch (replyError) {
178
- interactionLogger.error('[INTERACTION] Failed to send error reply:', replyError)
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
  }
@@ -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
- `- **Created**: ${formatDateTime(new Date(session.time.created))}`,
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 = typeof v === 'string' ? v.slice(0, 100) : JSON.stringify(v).slice(0, 100)
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(', ')