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
package/dist/tools.js CHANGED
@@ -11,7 +11,7 @@ const toolsLogger = createLogger('TOOLS');
11
11
  import { ShareMarkdown } from './markdown.js';
12
12
  import { formatDistanceToNow } from './utils.js';
13
13
  import pc from 'picocolors';
14
- import { initializeOpencodeForDirectory, getOpencodeSystemMessage, } from './discord-bot.js';
14
+ import { initializeOpencodeForDirectory, getOpencodeSystemMessage } from './discord-bot.js';
15
15
  export async function getTools({ onMessageCompleted, directory, }) {
16
16
  const getClient = await initializeOpencodeForDirectory(directory);
17
17
  const client = getClient();
@@ -83,23 +83,17 @@ export async function getTools({ onMessageCompleted, directory, }) {
83
83
  createNewChat: tool({
84
84
  description: 'Start a new chat session with an initial message. Does not wait for the message to complete',
85
85
  inputSchema: z.object({
86
- message: z
87
- .string()
88
- .describe('The initial message to start the chat with'),
86
+ message: z.string().describe('The initial message to start the chat with'),
89
87
  title: z.string().optional().describe('Optional title for the session'),
90
88
  model: z
91
89
  .object({
92
- providerId: z
93
- .string()
94
- .describe('The provider ID (e.g., "anthropic", "openai")'),
95
- modelId: z
96
- .string()
97
- .describe('The model ID (e.g., "claude-opus-4-20250514", "gpt-5")'),
90
+ providerId: z.string().describe('The provider ID (e.g., "anthropic", "openai")'),
91
+ modelId: z.string().describe('The model ID (e.g., "claude-opus-4-20250514", "gpt-5")'),
98
92
  })
99
93
  .optional()
100
94
  .describe('Optional model to use for this session'),
101
95
  }),
102
- execute: async ({ message, title, }) => {
96
+ execute: async ({ message, title }) => {
103
97
  if (!message.trim()) {
104
98
  throw new Error(`message must be a non empty string`);
105
99
  }
@@ -149,9 +143,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
149
143
  catch (error) {
150
144
  return {
151
145
  success: false,
152
- error: error instanceof Error
153
- ? error.message
154
- : 'Failed to create chat session',
146
+ error: error instanceof Error ? error.message : 'Failed to create chat session',
155
147
  };
156
148
  }
157
149
  },
@@ -180,8 +172,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
180
172
  });
181
173
  const messages = messagesResponse.data || [];
182
174
  const lastMessage = messages[messages.length - 1];
183
- if (lastMessage?.info.role === 'assistant' &&
184
- !lastMessage.info.time.completed) {
175
+ if (lastMessage?.info.role === 'assistant' && !lastMessage.info.time.completed) {
185
176
  return 'in_progress';
186
177
  }
187
178
  return 'finished';
@@ -228,10 +219,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
228
219
  description: 'Read messages from a chat session',
229
220
  inputSchema: z.object({
230
221
  sessionId: z.string().describe('The session ID to read messages from'),
231
- lastAssistantOnly: z
232
- .boolean()
233
- .optional()
234
- .describe('Only read the last assistant message'),
222
+ lastAssistantOnly: z.boolean().optional().describe('Only read the last assistant message'),
235
223
  }),
236
224
  execute: async ({ sessionId, lastAssistantOnly = false }) => {
237
225
  if (lastAssistantOnly) {
@@ -249,8 +237,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
249
237
  };
250
238
  }
251
239
  const lastMessage = assistantMessages[assistantMessages.length - 1];
252
- const status = 'completed' in lastMessage.info.time &&
253
- lastMessage.info.time.completed
240
+ const status = 'completed' in lastMessage.info.time && lastMessage.info.time.completed
254
241
  ? 'completed'
255
242
  : 'in_progress';
256
243
  const markdown = await markdownRenderer.generate({
@@ -13,7 +13,7 @@ import dedent from 'string-dedent';
13
13
  import { PermissionsBitField, Events, } from 'discord.js';
14
14
  import { createGenAIWorker } from './genai-worker-wrapper.js';
15
15
  import { getDatabase } from './database.js';
16
- import { sendThreadMessage, escapeDiscordFormatting, SILENT_MESSAGE_FLAGS } from './discord-utils.js';
16
+ import { sendThreadMessage, escapeDiscordFormatting, SILENT_MESSAGE_FLAGS, } from './discord-utils.js';
17
17
  import { transcribeAudio } from './voice.js';
18
18
  import { createLogger } from './logger.js';
19
19
  const voiceLogger = createLogger('VOICE');
@@ -189,7 +189,10 @@ export async function setupVoiceHandling({ connection, guildId, channelId, appId
189
189
  try {
190
190
  const textChannel = await discordClient.channels.fetch(textChannelRow.channel_id);
191
191
  if (textChannel?.isTextBased() && 'send' in textChannel) {
192
- await textChannel.send({ content: `⚠️ Voice session error: ${error}`, flags: SILENT_MESSAGE_FLAGS });
192
+ await textChannel.send({
193
+ content: `⚠️ Voice session error: ${error}`,
194
+ flags: SILENT_MESSAGE_FLAGS,
195
+ });
193
196
  }
194
197
  }
195
198
  catch (e) {
@@ -235,10 +238,7 @@ export async function setupVoiceHandling({ connection, guildId, channelId, appId
235
238
  },
236
239
  });
237
240
  const framer = frameMono16khz();
238
- const pipeline = audioStream
239
- .pipe(decoder)
240
- .pipe(downsampleTransform)
241
- .pipe(framer);
241
+ const pipeline = audioStream.pipe(decoder).pipe(downsampleTransform).pipe(framer);
242
242
  pipeline
243
243
  .on('data', (frame) => {
244
244
  if (currentSessionCount !== speakingSessionCount) {
@@ -404,8 +404,7 @@ export function registerVoiceStateHandler({ discordClient, appId, }) {
404
404
  voiceLogger.log(`Admin user ${member.user.tag} left voice channel: ${oldState.channel?.name}`);
405
405
  const guildId = guild.id;
406
406
  const voiceData = voiceConnections.get(guildId);
407
- if (voiceData &&
408
- voiceData.connection.joinConfig.channelId === oldState.channelId) {
407
+ if (voiceData && voiceData.connection.joinConfig.channelId === oldState.channelId) {
409
408
  const voiceChannel = oldState.channel;
410
409
  if (!voiceChannel)
411
410
  return;
@@ -433,8 +432,7 @@ export function registerVoiceStateHandler({ discordClient, appId, }) {
433
432
  voiceLogger.log(`Admin user ${member.user.tag} moved from ${oldState.channel?.name} to ${newState.channel?.name}`);
434
433
  const guildId = guild.id;
435
434
  const voiceData = voiceConnections.get(guildId);
436
- if (voiceData &&
437
- voiceData.connection.joinConfig.channelId === oldState.channelId) {
435
+ if (voiceData && voiceData.connection.joinConfig.channelId === oldState.channelId) {
438
436
  const oldVoiceChannel = oldState.channel;
439
437
  if (oldVoiceChannel) {
440
438
  const hasOtherAdmins = oldVoiceChannel.members.some((m) => {
@@ -472,8 +470,7 @@ export function registerVoiceStateHandler({ discordClient, appId, }) {
472
470
  return;
473
471
  const existingVoiceData = voiceConnections.get(newState.guild.id);
474
472
  if (existingVoiceData &&
475
- existingVoiceData.connection.state.status !==
476
- VoiceConnectionStatus.Destroyed) {
473
+ existingVoiceData.connection.state.status !== VoiceConnectionStatus.Destroyed) {
477
474
  voiceLogger.log(`Bot already connected to a voice channel in guild ${newState.guild.name}`);
478
475
  if (existingVoiceData.connection.joinConfig.channelId !== voiceChannel.id) {
479
476
  voiceLogger.log(`Moving bot from channel ${existingVoiceData.connection.joinConfig.channelId} to ${voiceChannel.id}`);
package/dist/voice.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Audio transcription service using Google Gemini.
2
2
  // Transcribes voice messages with code-aware context, using grep/glob tools
3
3
  // to verify technical terms, filenames, and function names in the codebase.
4
- import { GoogleGenAI, Type, } from '@google/genai';
4
+ import { GoogleGenAI, Type } from '@google/genai';
5
5
  import { createLogger } from './logger.js';
6
6
  import { glob } from 'glob';
7
7
  import { ripGrep } from 'ripgrep-js';
@@ -87,7 +87,7 @@ const transcriptionResultToolDeclaration = {
87
87
  required: ['transcription'],
88
88
  },
89
89
  };
90
- function createToolRunner({ directory, }) {
90
+ function createToolRunner({ directory }) {
91
91
  const hasDirectory = directory && directory.trim().length > 0;
92
92
  return async ({ name, args }) => {
93
93
  if (name === 'transcriptionResult') {
@@ -200,7 +200,9 @@ export async function runTranscriptionLoop({ genAI, model, initialContents, tool
200
200
  thinkingConfig: {
201
201
  thinkingBudget: 512,
202
202
  },
203
- tools: stepsRemaining <= 0 ? [{ functionDeclarations: [transcriptionResultToolDeclaration] }] : tools,
203
+ tools: stepsRemaining <= 0
204
+ ? [{ functionDeclarations: [transcriptionResultToolDeclaration] }]
205
+ : tools,
204
206
  },
205
207
  });
206
208
  }
package/dist/xml.js CHANGED
@@ -27,8 +27,7 @@ export function extractTagsArrays({ xml, tags, }) {
27
27
  // Extract content using original string positions
28
28
  const extractContent = () => {
29
29
  // Use element's own indices but exclude the tags
30
- if (element.startIndex !== null &&
31
- element.endIndex !== null) {
30
+ if (element.startIndex !== null && element.endIndex !== null) {
32
31
  // Extract the full element including tags
33
32
  const fullElement = xml.substring(element.startIndex, element.endIndex + 1);
34
33
  // Find where content starts (after opening tag)
@@ -57,8 +56,7 @@ export function extractTagsArrays({ xml, tags, }) {
57
56
  findTags(element.children, currentPath);
58
57
  }
59
58
  }
60
- else if (node.type === ElementType.Text &&
61
- node.parent?.type === ElementType.Root) {
59
+ else if (node.type === ElementType.Text && node.parent?.type === ElementType.Root) {
62
60
  const textNode = node;
63
61
  if (textNode.data.trim()) {
64
62
  // console.log('node.parent',node.parent)
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.35",
5
+ "version": "0.4.37",
6
6
  "scripts": {
7
7
  "dev": "tsx --env-file .env src/cli.ts",
8
8
  "prepublishOnly": "pnpm tsc",
@@ -11,7 +11,8 @@
11
11
  "test:events": "tsx test-events.ts",
12
12
  "pcm-to-mp3": "bun scripts/pcm-to-mp3",
13
13
  "test:send": "tsx send-test-message.ts",
14
- "register-commands": "tsx scripts/register-commands.ts"
14
+ "register-commands": "tsx scripts/register-commands.ts",
15
+ "format": "oxfmt src"
15
16
  },
16
17
  "repository": "https://github.com/remorses/kimaki",
17
18
  "bin": "bin.js",
@@ -1,35 +1,35 @@
1
1
  [Assistant]: Still not showing. The session might not have a `system` field and the first assistant message might not have `synthetic: true`. Let me add debug logging to see what's available:
2
2
 
3
- [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= // Get system prompt if requested
4
- if (includeSystemPrompt) {
5
- // try session.system fie, newString= // Get system prompt if requested
6
- if (includeSystemPrompt) {
7
- // try session.system fie
8
- [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= } else if (messages.length > 0) {
9
- // fallback: look for synthetic text part in first a, newString= } else if (messages.length > 0) {
10
- // fallback: look for synthetic text part in first a
3
+ [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= // Get system prompt if requested
4
+ if (includeSystemPrompt) {
5
+ // try session.system fie, newString= // Get system prompt if requested
6
+ if (includeSystemPrompt) {
7
+ // try session.system fie
8
+ [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= } else if (messages.length > 0) {
9
+ // fallback: look for synthetic text part in first a, newString= } else if (messages.length > 0) {
10
+ // fallback: look for synthetic text part in first a
11
11
  [Tool bash]: command=pnpm vitest run --testNamePattern "getCompactSessionContext generates" 2>&1 | tail -30, description=run test and check output, workdir=/Users/morse/Documents/GitHub/kimakivoice/discord, timeout=120000
12
12
  [Tool bash]: command=grep -i "session keys\|text parts" tmp/kimaki.log | tail -10, description=check debug output, workdir=/Users/morse/Documents/GitHub/kimakivoice/discord
13
13
  [Assistant]: Session doesn't have a `system` field. Let me fix the debug logging:
14
14
 
15
- [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= const textParts = (firstAssistant.parts || []).filter((p) => p.type === 'text')
16
- , newString= const textParts = (firstAssistant.parts || []).filter((p) => p.type === 'text')
17
-
15
+ [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= const textParts = (firstAssistant.parts || []).filter((p) => p.type === 'text')
16
+ , newString= const textParts = (firstAssistant.parts || []).filter((p) => p.type === 'text')
17
+
18
18
  [Tool bash]: command=pnpm vitest run --testNamePattern "getCompactSessionContext generates" 2>&1 && grep -i "text parts" , description=run test and check debug, workdir=/Users/morse/Documents/GitHub/kimakivoice/discord, timeout=120000
19
19
  [Assistant]: I see - the first assistant text part has **no `synthetic` field** (it's `undefined`), and it's only 44 chars. The session doesn't expose the system prompt.
20
20
 
21
21
  Let me remove debug logging and fix the test:
22
22
 
23
- [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= // Get system prompt if requested
24
- if (includeSystemPrompt) {
25
- // try session.system fie, newString= // Get system prompt if requested
26
- // Note: OpenCode SDK doesn't expose system prompt directl
27
- [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= try {
28
- // fetch session info to get system prompt
29
- const sessionResponse = await client.sess, newString= try {
30
- const messagesResponse = await client.session.messages({
31
- [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.test.ts, oldString= expect(context).toBeTruthy()
32
- expect(context.length).toBeGreaterThan(0)
33
- // should have tool cal, newString= expect(context).toBeTruthy()
34
- expect(context.length).toBeGreaterThan(0)
35
- // should have tool cal
23
+ [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= // Get system prompt if requested
24
+ if (includeSystemPrompt) {
25
+ // try session.system fie, newString= // Get system prompt if requested
26
+ // Note: OpenCode SDK doesn't expose system prompt directl
27
+ [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= try {
28
+ // fetch session info to get system prompt
29
+ const sessionResponse = await client.sess, newString= try {
30
+ const messagesResponse = await client.session.messages({
31
+ [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.test.ts, oldString= expect(context).toBeTruthy()
32
+ expect(context.length).toBeGreaterThan(0)
33
+ // should have tool cal, newString= expect(context).toBeTruthy()
34
+ expect(context.length).toBeGreaterThan(0)
35
+ // should have tool cal
@@ -2,46 +2,46 @@
2
2
  [Tool read]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, offset=248, limit=35
3
3
  [Assistant]: Let me update the code to also look for system role messages and fetch session info:
4
4
 
5
- [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= try {
6
- const messagesResponse = await client.session.messages({
7
- path: { id: sessionId },
8
- , newString= try {
9
- // fetch session info to get system prompt
10
- const sessionResponse = await client.sess
5
+ [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= try {
6
+ const messagesResponse = await client.session.messages({
7
+ path: { id: sessionId },
8
+ , newString= try {
9
+ // fetch session info to get system prompt
10
+ const sessionResponse = await client.sess
11
11
  [Tool bash]: command=pnpm vitest run -u --testNamePattern "getCompactSessionContext", description=run tests to check system prompt, workdir=/Users/morse/Documents/GitHub/kimakivoice/discord, timeout=120000
12
- [Tool bash]: command=head -30 discord/src/__snapshots__/compact-session-context.md, description=check if system prompt is in snapshot
12
+ [Tool bash]: command=head -30 discord/src/**snapshots**/compact-session-context.md, description=check if system prompt is in snapshot
13
13
  [Assistant]: Still not showing. The session might not have a `system` field and the first assistant message might not have `synthetic: true`. Let me add debug logging to see what's available:
14
14
 
15
- [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= // Get system prompt if requested
16
- if (includeSystemPrompt) {
17
- // try session.system fie, newString= // Get system prompt if requested
18
- if (includeSystemPrompt) {
19
- // try session.system fie
20
- [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= } else if (messages.length > 0) {
21
- // fallback: look for synthetic text part in first a, newString= } else if (messages.length > 0) {
22
- // fallback: look for synthetic text part in first a
15
+ [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= // Get system prompt if requested
16
+ if (includeSystemPrompt) {
17
+ // try session.system fie, newString= // Get system prompt if requested
18
+ if (includeSystemPrompt) {
19
+ // try session.system fie
20
+ [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= } else if (messages.length > 0) {
21
+ // fallback: look for synthetic text part in first a, newString= } else if (messages.length > 0) {
22
+ // fallback: look for synthetic text part in first a
23
23
  [Tool bash]: command=pnpm vitest run --testNamePattern "getCompactSessionContext generates" 2>&1 | tail -30, description=run test and check output, workdir=/Users/morse/Documents/GitHub/kimakivoice/discord, timeout=120000
24
24
  [Tool bash]: command=grep -i "session keys\|text parts" tmp/kimaki.log | tail -10, description=check debug output, workdir=/Users/morse/Documents/GitHub/kimakivoice/discord
25
25
  [Assistant]: Session doesn't have a `system` field. Let me fix the debug logging:
26
26
 
27
- [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= const textParts = (firstAssistant.parts || []).filter((p) => p.type === 'text')
28
- , newString= const textParts = (firstAssistant.parts || []).filter((p) => p.type === 'text')
29
-
27
+ [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= const textParts = (firstAssistant.parts || []).filter((p) => p.type === 'text')
28
+ , newString= const textParts = (firstAssistant.parts || []).filter((p) => p.type === 'text')
29
+
30
30
  [Tool bash]: command=pnpm vitest run --testNamePattern "getCompactSessionContext generates" 2>&1 && grep -i "text parts" , description=run test and check debug, workdir=/Users/morse/Documents/GitHub/kimakivoice/discord, timeout=120000
31
31
  [Assistant]: I see - the first assistant text part has **no `synthetic` field** (it's `undefined`), and it's only 44 chars. The session doesn't expose the system prompt.
32
32
 
33
33
  Let me remove debug logging and fix the test:
34
34
 
35
- [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= // Get system prompt if requested
36
- if (includeSystemPrompt) {
37
- // try session.system fie, newString= // Get system prompt if requested
38
- // Note: OpenCode SDK doesn't expose system prompt directl
39
- [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= try {
40
- // fetch session info to get system prompt
41
- const sessionResponse = await client.sess, newString= try {
42
- const messagesResponse = await client.session.messages({
43
- [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.test.ts, oldString= expect(context).toBeTruthy()
44
- expect(context.length).toBeGreaterThan(0)
45
- // should have tool cal, newString= expect(context).toBeTruthy()
46
- expect(context.length).toBeGreaterThan(0)
47
- // should have tool cal
35
+ [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= // Get system prompt if requested
36
+ if (includeSystemPrompt) {
37
+ // try session.system fie, newString= // Get system prompt if requested
38
+ // Note: OpenCode SDK doesn't expose system prompt directl
39
+ [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts, oldString= try {
40
+ // fetch session info to get system prompt
41
+ const sessionResponse = await client.sess, newString= try {
42
+ const messagesResponse = await client.session.messages({
43
+ [Tool edit]: filePath=/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.test.ts, oldString= expect(context).toBeTruthy()
44
+ expect(context.length).toBeGreaterThan(0)
45
+ // should have tool cal, newString= expect(context).toBeTruthy()
46
+ expect(context.length).toBeGreaterThan(0)
47
+ // should have tool cal
@@ -119,9 +119,7 @@ function jsonSchemaToGenAISchema(jsonSchema: any): Schema {
119
119
  /**
120
120
  * Convert AI SDK Tool to GenAI FunctionDeclaration
121
121
  */
122
- export function aiToolToGenAIFunction(
123
- tool: Tool<any, any>,
124
- ): FunctionDeclaration {
122
+ export function aiToolToGenAIFunction(tool: Tool<any, any>): FunctionDeclaration {
125
123
  // Extract the input schema - assume it's a Zod schema
126
124
  const inputSchema = tool.inputSchema as z.ZodType<any>
127
125
 
@@ -183,11 +181,7 @@ export function aiToolToCallableTool(
183
181
 
184
182
  for (const functionCall of functionCalls) {
185
183
  // Check if this function call matches our tool
186
- if (
187
- functionCall.name !== toolName &&
188
- name &&
189
- functionCall.name !== name
190
- ) {
184
+ if (functionCall.name !== toolName && name && functionCall.name !== name) {
191
185
  continue
192
186
  }
193
187
 
@@ -249,7 +243,5 @@ export function extractSchemaFromTool(tool: Tool<any, any>): any {
249
243
  export function callableToolsFromObject(
250
244
  tools: Record<string, Tool<any, any>>,
251
245
  ): Array<CallableTool & { name: string }> {
252
- return Object.entries(tools).map(([name, tool]) =>
253
- aiToolToCallableTool(tool, name),
254
- )
246
+ return Object.entries(tools).map(([name, tool]) => aiToolToCallableTool(tool, name))
255
247
  }
@@ -2,12 +2,7 @@
2
2
  // Creates and manages Kimaki project channels (text + voice pairs),
3
3
  // extracts channel metadata from topic tags, and ensures category structure.
4
4
 
5
- import {
6
- ChannelType,
7
- type CategoryChannel,
8
- type Guild,
9
- type TextChannel,
10
- } from 'discord.js'
5
+ import { ChannelType, type CategoryChannel, type Guild, type TextChannel } from 'discord.js'
11
6
  import path from 'node:path'
12
7
  import { getDatabase } from './database.js'
13
8
  import { extractTagsArrays } from './xml.js'
@@ -18,15 +13,13 @@ export async function ensureKimakiCategory(
18
13
  ): Promise<CategoryChannel> {
19
14
  const categoryName = botName ? `Kimaki ${botName}` : 'Kimaki'
20
15
 
21
- const existingCategory = guild.channels.cache.find(
22
- (channel): channel is CategoryChannel => {
23
- if (channel.type !== ChannelType.GuildCategory) {
24
- return false
25
- }
16
+ const existingCategory = guild.channels.cache.find((channel): channel is CategoryChannel => {
17
+ if (channel.type !== ChannelType.GuildCategory) {
18
+ return false
19
+ }
26
20
 
27
- return channel.name.toLowerCase() === categoryName.toLowerCase()
28
- },
29
- )
21
+ return channel.name.toLowerCase() === categoryName.toLowerCase()
22
+ })
30
23
 
31
24
  if (existingCategory) {
32
25
  return existingCategory
@@ -44,15 +37,13 @@ export async function ensureKimakiAudioCategory(
44
37
  ): Promise<CategoryChannel> {
45
38
  const categoryName = botName ? `Kimaki Audio ${botName}` : 'Kimaki Audio'
46
39
 
47
- const existingCategory = guild.channels.cache.find(
48
- (channel): channel is CategoryChannel => {
49
- if (channel.type !== ChannelType.GuildCategory) {
50
- return false
51
- }
40
+ const existingCategory = guild.channels.cache.find((channel): channel is CategoryChannel => {
41
+ if (channel.type !== ChannelType.GuildCategory) {
42
+ return false
43
+ }
52
44
 
53
- return channel.name.toLowerCase() === categoryName.toLowerCase()
54
- },
55
- )
45
+ return channel.name.toLowerCase() === categoryName.toLowerCase()
46
+ })
56
47
 
57
48
  if (existingCategory) {
58
49
  return existingCategory
@@ -99,15 +90,15 @@ export async function createProjectChannels({
99
90
 
100
91
  getDatabase()
101
92
  .prepare(
102
- 'INSERT OR REPLACE INTO channel_directories (channel_id, directory, channel_type) VALUES (?, ?, ?)',
93
+ 'INSERT OR REPLACE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)',
103
94
  )
104
- .run(textChannel.id, projectDirectory, 'text')
95
+ .run(textChannel.id, projectDirectory, 'text', appId)
105
96
 
106
97
  getDatabase()
107
98
  .prepare(
108
- 'INSERT OR REPLACE INTO channel_directories (channel_id, directory, channel_type) VALUES (?, ?, ?)',
99
+ 'INSERT OR REPLACE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)',
109
100
  )
110
- .run(voiceChannel.id, projectDirectory, 'voice')
101
+ .run(voiceChannel.id, projectDirectory, 'voice', appId)
111
102
 
112
103
  return {
113
104
  textChannelId: textChannel.id,
@@ -124,9 +115,7 @@ export type ChannelWithTags = {
124
115
  kimakiApp?: string
125
116
  }
126
117
 
127
- export async function getChannelsWithDescriptions(
128
- guild: Guild,
129
- ): Promise<ChannelWithTags[]> {
118
+ export async function getChannelsWithDescriptions(guild: Guild): Promise<ChannelWithTags[]> {
130
119
  const channels: ChannelWithTags[] = []
131
120
 
132
121
  guild.channels.cache