kimaki 0.4.45 → 0.4.47

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 (79) hide show
  1. package/dist/cli.js +27 -2
  2. package/dist/commands/abort.js +2 -2
  3. package/dist/commands/add-project.js +2 -2
  4. package/dist/commands/agent.js +4 -4
  5. package/dist/commands/ask-question.js +9 -8
  6. package/dist/commands/compact.js +126 -0
  7. package/dist/commands/create-new-project.js +5 -3
  8. package/dist/commands/fork.js +5 -3
  9. package/dist/commands/merge-worktree.js +2 -2
  10. package/dist/commands/model.js +5 -5
  11. package/dist/commands/permissions.js +2 -2
  12. package/dist/commands/queue.js +2 -2
  13. package/dist/commands/remove-project.js +2 -2
  14. package/dist/commands/resume.js +4 -2
  15. package/dist/commands/session.js +4 -2
  16. package/dist/commands/share.js +2 -2
  17. package/dist/commands/undo-redo.js +2 -2
  18. package/dist/commands/user-command.js +4 -2
  19. package/dist/commands/verbosity.js +3 -3
  20. package/dist/commands/worktree-settings.js +2 -2
  21. package/dist/commands/worktree.js +20 -8
  22. package/dist/database.js +2 -2
  23. package/dist/discord-bot.js +5 -3
  24. package/dist/discord-utils.js +2 -2
  25. package/dist/genai-worker-wrapper.js +3 -3
  26. package/dist/genai-worker.js +2 -2
  27. package/dist/genai.js +2 -2
  28. package/dist/interaction-handler.js +6 -2
  29. package/dist/logger.js +57 -9
  30. package/dist/markdown.js +2 -2
  31. package/dist/message-formatting.js +69 -6
  32. package/dist/openai-realtime.js +2 -2
  33. package/dist/opencode.js +2 -2
  34. package/dist/session-handler.js +93 -15
  35. package/dist/tools.js +2 -2
  36. package/dist/voice-handler.js +2 -2
  37. package/dist/voice.js +2 -2
  38. package/dist/worktree-utils.js +91 -7
  39. package/dist/xml.js +2 -2
  40. package/package.json +1 -1
  41. package/src/cli.ts +28 -2
  42. package/src/commands/abort.ts +2 -2
  43. package/src/commands/add-project.ts +2 -2
  44. package/src/commands/agent.ts +4 -4
  45. package/src/commands/ask-question.ts +9 -8
  46. package/src/commands/compact.ts +148 -0
  47. package/src/commands/create-new-project.ts +6 -3
  48. package/src/commands/fork.ts +6 -3
  49. package/src/commands/merge-worktree.ts +2 -2
  50. package/src/commands/model.ts +5 -5
  51. package/src/commands/permissions.ts +2 -2
  52. package/src/commands/queue.ts +2 -2
  53. package/src/commands/remove-project.ts +2 -2
  54. package/src/commands/resume.ts +5 -2
  55. package/src/commands/session.ts +5 -2
  56. package/src/commands/share.ts +2 -2
  57. package/src/commands/undo-redo.ts +2 -2
  58. package/src/commands/user-command.ts +5 -2
  59. package/src/commands/verbosity.ts +3 -3
  60. package/src/commands/worktree-settings.ts +2 -2
  61. package/src/commands/worktree.ts +23 -7
  62. package/src/database.ts +2 -2
  63. package/src/discord-bot.ts +6 -3
  64. package/src/discord-utils.ts +2 -2
  65. package/src/genai-worker-wrapper.ts +3 -3
  66. package/src/genai-worker.ts +2 -2
  67. package/src/genai.ts +2 -2
  68. package/src/interaction-handler.ts +7 -2
  69. package/src/logger.ts +64 -10
  70. package/src/markdown.ts +2 -2
  71. package/src/message-formatting.ts +82 -6
  72. package/src/openai-realtime.ts +2 -2
  73. package/src/opencode.ts +2 -2
  74. package/src/session-handler.ts +105 -15
  75. package/src/tools.ts +2 -2
  76. package/src/voice-handler.ts +2 -2
  77. package/src/voice.ts +2 -2
  78. package/src/worktree-utils.ts +111 -7
  79. package/src/xml.ts +2 -2
@@ -5,10 +5,10 @@ import type { CommandContext } from './types.js'
5
5
  import { getDatabase } from '../database.js'
6
6
  import { initializeOpencodeForDirectory } from '../opencode.js'
7
7
  import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
  import * as errore from 'errore'
10
10
 
11
- const logger = createLogger('SHARE')
11
+ const logger = createLogger(LogPrefix.SHARE)
12
12
 
13
13
  export async function handleShareCommand({ command }: CommandContext): Promise<void> {
14
14
  const channel = command.channel
@@ -5,10 +5,10 @@ import type { CommandContext } from './types.js'
5
5
  import { getDatabase } from '../database.js'
6
6
  import { initializeOpencodeForDirectory } from '../opencode.js'
7
7
  import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
  import * as errore from 'errore'
10
10
 
11
- const logger = createLogger('UNDO-REDO')
11
+ const logger = createLogger(LogPrefix.UNDO_REDO)
12
12
 
13
13
  export async function handleUndoCommand({ command }: CommandContext): Promise<void> {
14
14
  const channel = command.channel
@@ -5,11 +5,11 @@ import type { CommandContext, CommandHandler } from './types.js'
5
5
  import { ChannelType, type TextChannel, type ThreadChannel } from 'discord.js'
6
6
  import { handleOpencodeSession } from '../session-handler.js'
7
7
  import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
  import { getDatabase, getChannelDirectory } from '../database.js'
10
10
  import fs from 'node:fs'
11
11
 
12
- const userCommandLogger = createLogger('USER_CMD')
12
+ const userCommandLogger = createLogger(LogPrefix.USER_CMD)
13
13
 
14
14
  export const handleUserCommand: CommandHandler = async ({ command, appId }: CommandContext) => {
15
15
  const discordCommandName = command.commandName
@@ -136,6 +136,9 @@ export const handleUserCommand: CommandHandler = async ({ command, appId }: Comm
136
136
  reason: `OpenCode command: ${commandName}`,
137
137
  })
138
138
 
139
+ // Add user to thread so it appears in their sidebar
140
+ await newThread.members.add(command.user.id)
141
+
139
142
  await command.editReply(`Started /${commandName} in ${newThread.toString()}`)
140
143
 
141
144
  await handleOpencodeSession({
@@ -5,9 +5,9 @@
5
5
 
6
6
  import { ChatInputCommandInteraction, ChannelType, type TextChannel, type ThreadChannel } from 'discord.js'
7
7
  import { getChannelVerbosity, setChannelVerbosity, type VerbosityLevel } from '../database.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
 
10
- const verbosityLogger = createLogger('VERBOSITY')
10
+ const verbosityLogger = createLogger(LogPrefix.VERBOSITY)
11
11
 
12
12
  /**
13
13
  * Handle the /verbosity slash command.
@@ -65,7 +65,7 @@ export async function handleVerbosityCommand({
65
65
  : 'All output will be shown, including tool executions and status messages.'
66
66
 
67
67
  await command.reply({
68
- content: `Verbosity set to **${level}**.\n\n${description}\n\nThis applies to all new sessions in this channel.`,
68
+ content: `Verbosity set to **${level}**.\n${description}\nThis applies to all new sessions in this channel.`,
69
69
  ephemeral: true,
70
70
  })
71
71
  }
@@ -5,9 +5,9 @@
5
5
  import { ChatInputCommandInteraction, ChannelType, type TextChannel } from 'discord.js'
6
6
  import { getChannelWorktreesEnabled, setChannelWorktreesEnabled } from '../database.js'
7
7
  import { getKimakiMetadata } from '../discord-utils.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
 
10
- const worktreeSettingsLogger = createLogger('WORKTREE_SETTINGS')
10
+ const worktreeSettingsLogger = createLogger(LogPrefix.WORKTREE)
11
11
 
12
12
  /**
13
13
  * Handle the /enable-worktrees slash command.
@@ -14,12 +14,12 @@ import {
14
14
  } from '../database.js'
15
15
  import { initializeOpencodeForDirectory, getOpencodeClientV2 } from '../opencode.js'
16
16
  import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
17
- import { createLogger } from '../logger.js'
18
- import { createWorktreeWithSubmodules } from '../worktree-utils.js'
17
+ import { createLogger, LogPrefix } from '../logger.js'
18
+ import { createWorktreeWithSubmodules, captureGitDiff, type CapturedDiff } from '../worktree-utils.js'
19
19
  import { WORKTREE_PREFIX } from './merge-worktree.js'
20
20
  import * as errore from 'errore'
21
21
 
22
- const logger = createLogger('WORKTREE')
22
+ const logger = createLogger(LogPrefix.WORKTREE)
23
23
 
24
24
  class WorktreeError extends Error {
25
25
  constructor(message: string, options?: { cause?: unknown }) {
@@ -91,6 +91,7 @@ function getProjectDirectoryFromChannel(
91
91
 
92
92
  /**
93
93
  * Create worktree in background and update starter message when done.
94
+ * If diff is provided, it's applied during worktree creation (before submodule init).
94
95
  */
95
96
  async function createWorktreeInBackground({
96
97
  thread,
@@ -98,19 +99,22 @@ async function createWorktreeInBackground({
98
99
  worktreeName,
99
100
  projectDirectory,
100
101
  clientV2,
102
+ diff,
101
103
  }: {
102
104
  thread: ThreadChannel
103
105
  starterMessage: Message
104
106
  worktreeName: string
105
107
  projectDirectory: string
106
108
  clientV2: ReturnType<typeof getOpencodeClientV2> & {}
109
+ diff?: CapturedDiff | null
107
110
  }): Promise<void> {
108
- // Create worktree using SDK v2 and init submodules
111
+ // Create worktree using SDK v2, apply diff, then init submodules
109
112
  logger.log(`Creating worktree "${worktreeName}" for project ${projectDirectory}`)
110
113
  const worktreeResult = await createWorktreeWithSubmodules({
111
114
  clientV2,
112
115
  directory: projectDirectory,
113
116
  name: worktreeName,
117
+ diff,
114
118
  })
115
119
 
116
120
  if (worktreeResult instanceof Error) {
@@ -123,10 +127,12 @@ async function createWorktreeInBackground({
123
127
 
124
128
  // Success - update database and edit starter message
125
129
  setWorktreeReady({ threadId: thread.id, worktreeDirectory: worktreeResult.directory })
130
+ const diffStatus = diff ? (worktreeResult.diffApplied ? '\n✅ Changes applied' : '\n⚠️ Failed to apply changes') : ''
126
131
  await starterMessage.edit(
127
132
  `🌳 **Worktree: ${worktreeName}**\n` +
128
133
  `📁 \`${worktreeResult.directory}\`\n` +
129
- `🌿 Branch: \`${worktreeResult.branch}\``
134
+ `🌿 Branch: \`${worktreeResult.branch}\`` +
135
+ diffStatus
130
136
  )
131
137
  }
132
138
 
@@ -228,6 +234,9 @@ export async function handleNewWorktreeCommand({
228
234
  reason: 'Worktree session',
229
235
  })
230
236
 
237
+ // Add user to thread so it appears in their sidebar
238
+ await thread.members.add(command.user.id)
239
+
231
240
  return { thread, starterMessage }
232
241
  },
233
242
  catch: (e) => new WorktreeError('Failed to create thread', { cause: e }),
@@ -334,6 +343,11 @@ async function handleWorktreeInThread({
334
343
  return
335
344
  }
336
345
 
346
+ // Capture git diff from project directory before creating worktree.
347
+ // This allows transferring uncommitted changes to the new worktree.
348
+ const diff = await captureGitDiff(projectDirectory)
349
+ const hasDiff = diff && (diff.staged || diff.unstaged)
350
+
337
351
  // Store pending worktree in database for this existing thread
338
352
  createPendingWorktree({
339
353
  threadId: thread.id,
@@ -342,20 +356,22 @@ async function handleWorktreeInThread({
342
356
  })
343
357
 
344
358
  // Send status message in thread
359
+ const diffNote = hasDiff ? '\n📋 Will transfer uncommitted changes' : ''
345
360
  const statusMessage = await thread.send({
346
- content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...`,
361
+ content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...${diffNote}`,
347
362
  flags: SILENT_MESSAGE_FLAGS,
348
363
  })
349
364
 
350
365
  await command.editReply(`Creating worktree \`${worktreeName}\` for this thread...`)
351
366
 
352
- // Create worktree in background
367
+ // Create worktree in background, passing diff to apply after creation
353
368
  createWorktreeInBackground({
354
369
  thread,
355
370
  starterMessage: statusMessage,
356
371
  worktreeName,
357
372
  projectDirectory,
358
373
  clientV2,
374
+ diff,
359
375
  }).catch((e) => {
360
376
  logger.error('[NEW-WORKTREE] Background error:', e)
361
377
  })
package/src/database.ts CHANGED
@@ -6,10 +6,10 @@ import Database from 'better-sqlite3'
6
6
  import fs from 'node:fs'
7
7
  import path from 'node:path'
8
8
  import * as errore from 'errore'
9
- import { createLogger } from './logger.js'
9
+ import { createLogger, LogPrefix } from './logger.js'
10
10
  import { getDataDir } from './config.js'
11
11
 
12
- const dbLogger = createLogger('DB')
12
+ const dbLogger = createLogger(LogPrefix.DB)
13
13
 
14
14
  let db: Database.Database | null = null
15
15
 
@@ -66,7 +66,7 @@ import {
66
66
  } from 'discord.js'
67
67
  import fs from 'node:fs'
68
68
  import * as errore from 'errore'
69
- import { createLogger } from './logger.js'
69
+ import { createLogger, LogPrefix } from './logger.js'
70
70
  import { setGlobalDispatcher, Agent } from 'undici'
71
71
 
72
72
  // Increase connection pool to prevent deadlock when multiple sessions have open SSE streams.
@@ -74,8 +74,8 @@ import { setGlobalDispatcher, Agent } from 'undici'
74
74
  // regular HTTP requests (question.reply, session.prompt) get blocked → deadlock.
75
75
  setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0, connections: 500 }))
76
76
 
77
- const discordLogger = createLogger('DISCORD')
78
- const voiceLogger = createLogger('VOICE')
77
+ const discordLogger = createLogger(LogPrefix.DISCORD)
78
+ const voiceLogger = createLogger(LogPrefix.VOICE)
79
79
 
80
80
  type StartOptions = {
81
81
  token: string
@@ -425,6 +425,9 @@ export async function startDiscordBot({
425
425
  reason: 'Start Claude session',
426
426
  })
427
427
 
428
+ // Add user to thread so it appears in their sidebar
429
+ await thread.members.add(message.author.id)
430
+
428
431
  discordLogger.log(`Created thread "${thread.name}" (${thread.id})`)
429
432
 
430
433
  // Create worktree if worktrees are enabled (CLI flag OR channel setting)
@@ -8,12 +8,12 @@ import { formatMarkdownTables } from './format-tables.js'
8
8
  import { getChannelDirectory } from './database.js'
9
9
  import { limitHeadingDepth } from './limit-heading-depth.js'
10
10
  import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js'
11
- import { createLogger } from './logger.js'
11
+ import { createLogger, LogPrefix } from './logger.js'
12
12
  import mime from 'mime'
13
13
  import fs from 'node:fs'
14
14
  import path from 'node:path'
15
15
 
16
- const discordLogger = createLogger('DISCORD')
16
+ const discordLogger = createLogger(LogPrefix.DISCORD)
17
17
 
18
18
  export const SILENT_MESSAGE_FLAGS = 4 | 4096
19
19
  // Same as SILENT but without SuppressNotifications - triggers badge/notification
@@ -5,10 +5,10 @@
5
5
  import { Worker } from 'node:worker_threads'
6
6
  import type { WorkerInMessage, WorkerOutMessage } from './worker-types.js'
7
7
  import type { Tool as AITool } from 'ai'
8
- import { createLogger } from './logger.js'
8
+ import { createLogger, LogPrefix } from './logger.js'
9
9
 
10
- const genaiWorkerLogger = createLogger('GENAI WORKER')
11
- const genaiWrapperLogger = createLogger('GENAI WORKER WRAPPER')
10
+ const genaiWorkerLogger = createLogger(LogPrefix.GENAI_WORKER)
11
+ const genaiWrapperLogger = createLogger(LogPrefix.GENAI_WORKER)
12
12
 
13
13
  export interface GenAIWorkerOptions {
14
14
  directory: string
@@ -13,13 +13,13 @@ import type { Session } from '@google/genai'
13
13
  import { getTools } from './tools.js'
14
14
  import { mkdir } from 'node:fs/promises'
15
15
  import type { WorkerInMessage, WorkerOutMessage } from './worker-types.js'
16
- import { createLogger } from './logger.js'
16
+ import { createLogger, LogPrefix } from './logger.js'
17
17
 
18
18
  if (!parentPort) {
19
19
  throw new Error('This module must be run as a worker thread')
20
20
  }
21
21
 
22
- const workerLogger = createLogger(`WORKER ${threadId}`)
22
+ const workerLogger = createLogger(`${LogPrefix.WORKER}_${threadId}`)
23
23
  workerLogger.log('GenAI worker started')
24
24
 
25
25
  // Define sendError early so it can be used by global handlers
package/src/genai.ts CHANGED
@@ -7,10 +7,10 @@ import type { CallableTool } from '@google/genai'
7
7
  import { writeFile } from 'fs'
8
8
  import type { Tool as AITool } from 'ai'
9
9
 
10
- import { createLogger } from './logger.js'
10
+ import { createLogger, LogPrefix } from './logger.js'
11
11
  import { aiToolToCallableTool } from './ai-tool-to-genai.js'
12
12
 
13
- const genaiLogger = createLogger('GENAI')
13
+ const genaiLogger = createLogger(LogPrefix.GENAI)
14
14
 
15
15
  const audioParts: Buffer[] = []
16
16
 
@@ -19,6 +19,7 @@ import {
19
19
  import { handleCreateNewProjectCommand } from './commands/create-new-project.js'
20
20
  import { handlePermissionSelectMenu } from './commands/permissions.js'
21
21
  import { handleAbortCommand } from './commands/abort.js'
22
+ import { handleCompactCommand } from './commands/compact.js'
22
23
  import { handleShareCommand } from './commands/share.js'
23
24
  import { handleForkCommand, handleForkSelectMenu } from './commands/fork.js'
24
25
  import {
@@ -32,9 +33,9 @@ import { handleQueueCommand, handleClearQueueCommand } from './commands/queue.js
32
33
  import { handleUndoCommand, handleRedoCommand } from './commands/undo-redo.js'
33
34
  import { handleUserCommand } from './commands/user-command.js'
34
35
  import { handleVerbosityCommand } from './commands/verbosity.js'
35
- import { createLogger } from './logger.js'
36
+ import { createLogger, LogPrefix } from './logger.js'
36
37
 
37
- const interactionLogger = createLogger('INTERACTION')
38
+ const interactionLogger = createLogger(LogPrefix.INTERACTION)
38
39
 
39
40
  export function registerInteractionHandler({
40
41
  discordClient,
@@ -126,6 +127,10 @@ export function registerInteractionHandler({
126
127
  await handleAbortCommand({ command: interaction, appId })
127
128
  return
128
129
 
130
+ case 'compact':
131
+ await handleCompactCommand({ command: interaction, appId })
132
+ return
133
+
129
134
  case 'share':
130
135
  await handleShareCommand({ command: interaction, appId })
131
136
  return
package/src/logger.ts CHANGED
@@ -1,12 +1,54 @@
1
- // Prefixed logging utility using @clack/prompts.
2
- // Creates loggers with consistent prefixes for different subsystems
3
- // (DISCORD, VOICE, SESSION, etc.) for easier debugging.
1
+ // Prefixed logging utility.
2
+ // Uses picocolors for compact frequent logs (log, info, debug).
3
+ // Uses @clack/prompts only for important events (warn, error) with visual distinction.
4
4
 
5
- import { log } from '@clack/prompts'
5
+ import { log as clackLog } from '@clack/prompts'
6
6
  import fs from 'node:fs'
7
7
  import path, { dirname } from 'node:path'
8
8
  import { fileURLToPath } from 'node:url'
9
9
  import util from 'node:util'
10
+ import pc from 'picocolors'
11
+
12
+ // All known log prefixes - add new ones here to keep alignment consistent
13
+ export const LogPrefix = {
14
+ ABORT: 'ABORT',
15
+ ADD_PROJECT: 'ADD_PROJ',
16
+ AGENT: 'AGENT',
17
+ ASK_QUESTION: 'QUESTION',
18
+ CLI: 'CLI',
19
+ COMPACT: 'COMPACT',
20
+ CREATE_PROJECT: 'NEW_PROJ',
21
+ DB: 'DB',
22
+ DISCORD: 'DISCORD',
23
+ FORK: 'FORK',
24
+ FORMATTING: 'FORMAT',
25
+ GENAI: 'GENAI',
26
+ GENAI_WORKER: 'GENAI_W',
27
+ INTERACTION: 'INTERACT',
28
+ MARKDOWN: 'MARKDOWN',
29
+ MODEL: 'MODEL',
30
+ OPENAI: 'OPENAI',
31
+ OPENCODE: 'OPENCODE',
32
+ PERMISSIONS: 'PERMS',
33
+ QUEUE: 'QUEUE',
34
+ REMOVE_PROJECT: 'RM_PROJ',
35
+ RESUME: 'RESUME',
36
+ SESSION: 'SESSION',
37
+ SHARE: 'SHARE',
38
+ TOOLS: 'TOOLS',
39
+ UNDO_REDO: 'UNDO',
40
+ USER_CMD: 'USER_CMD',
41
+ VERBOSITY: 'VERBOSE',
42
+ VOICE: 'VOICE',
43
+ WORKER: 'WORKER',
44
+ WORKTREE: 'WORKTREE',
45
+ XML: 'XML',
46
+ } as const
47
+
48
+ export type LogPrefixType = (typeof LogPrefix)[keyof typeof LogPrefix]
49
+
50
+ // compute max length from all known prefixes for alignment
51
+ const MAX_PREFIX_LENGTH = Math.max(...Object.values(LogPrefix).map((p) => p.length))
10
52
 
11
53
  const __filename = fileURLToPath(import.meta.url)
12
54
  const __dirname = dirname(__filename)
@@ -39,27 +81,39 @@ function writeToFile(level: string, prefix: string, args: unknown[]) {
39
81
  fs.appendFileSync(logFilePath, message)
40
82
  }
41
83
 
42
- export function createLogger(prefix: string) {
84
+ function getTimestamp(): string {
85
+ const now = new Date()
86
+ return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
87
+ }
88
+
89
+ function padPrefix(prefix: string): string {
90
+ return prefix.padEnd(MAX_PREFIX_LENGTH)
91
+ }
92
+
93
+ export function createLogger(prefix: LogPrefixType | string) {
94
+ const paddedPrefix = padPrefix(prefix)
43
95
  return {
44
96
  log: (...args: unknown[]) => {
45
97
  writeToFile('INFO', prefix, args)
46
- log.info([`[${prefix}]`, ...args.map(formatArg)].join(' '))
98
+ console.log(pc.dim(getTimestamp()), pc.cyan(paddedPrefix), ...args.map(formatArg))
47
99
  },
48
100
  error: (...args: unknown[]) => {
49
101
  writeToFile('ERROR', prefix, args)
50
- log.error([`[${prefix}]`, ...args.map(formatArg)].join(' '))
102
+ // use clack for errors - visually distinct
103
+ clackLog.error([paddedPrefix, ...args.map(formatArg)].join(' '))
51
104
  },
52
105
  warn: (...args: unknown[]) => {
53
106
  writeToFile('WARN', prefix, args)
54
- log.warn([`[${prefix}]`, ...args.map(formatArg)].join(' '))
107
+ // use clack for warnings - visually distinct
108
+ clackLog.warn([paddedPrefix, ...args.map(formatArg)].join(' '))
55
109
  },
56
110
  info: (...args: unknown[]) => {
57
111
  writeToFile('INFO', prefix, args)
58
- log.info([`[${prefix}]`, ...args.map(formatArg)].join(' '))
112
+ console.log(pc.dim(getTimestamp()), pc.blue(paddedPrefix), ...args.map(formatArg))
59
113
  },
60
114
  debug: (...args: unknown[]) => {
61
115
  writeToFile('DEBUG', prefix, args)
62
- log.info([`[${prefix}]`, ...args.map(formatArg)].join(' '))
116
+ console.log(pc.dim(getTimestamp()), pc.dim(paddedPrefix), ...args.map(formatArg))
63
117
  },
64
118
  }
65
119
  }
package/src/markdown.ts CHANGED
@@ -9,7 +9,7 @@ import { createTaggedError } from 'errore'
9
9
  import * as yaml from 'js-yaml'
10
10
  import { formatDateTime } from './utils.js'
11
11
  import { extractNonXmlContent } from './xml.js'
12
- import { createLogger } from './logger.js'
12
+ import { createLogger, LogPrefix } from './logger.js'
13
13
  import { SessionNotFoundError, MessagesNotFoundError } from './errors.js'
14
14
 
15
15
  // Generic error for unexpected exceptions in async operations
@@ -18,7 +18,7 @@ class UnexpectedError extends createTaggedError({
18
18
  message: '$message',
19
19
  }) {}
20
20
 
21
- const markdownLogger = createLogger('MARKDOWN')
21
+ const markdownLogger = createLogger(LogPrefix.MARKDOWN)
22
22
 
23
23
  export class ShareMarkdown {
24
24
  constructor(private client: OpencodeClient) {}
@@ -8,7 +8,7 @@ import type { Message } from 'discord.js'
8
8
  import fs from 'node:fs'
9
9
  import path from 'node:path'
10
10
  import * as errore from 'errore'
11
- import { createLogger } from './logger.js'
11
+ import { createLogger, LogPrefix } from './logger.js'
12
12
  import { FetchError } from './errors.js'
13
13
 
14
14
  // Generic message type compatible with both v1 and v2 SDK
@@ -19,7 +19,7 @@ type GenericSessionMessage = {
19
19
 
20
20
  const ATTACHMENTS_DIR = path.join(process.cwd(), 'tmp', 'discord-attachments')
21
21
 
22
- const logger = createLogger('FORMATTING')
22
+ const logger = createLogger(LogPrefix.FORMATTING)
23
23
 
24
24
  /**
25
25
  * Escapes Discord inline markdown characters so dynamic content
@@ -29,6 +29,13 @@ function escapeInlineMarkdown(text: string): string {
29
29
  return text.replace(/([*_~|`\\])/g, '\\$1')
30
30
  }
31
31
 
32
+ /**
33
+ * Normalize whitespace: convert newlines to spaces and collapse consecutive spaces.
34
+ */
35
+ function normalizeWhitespace(text: string): string {
36
+ return text.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ')
37
+ }
38
+
32
39
  /**
33
40
  * Collects and formats the last N assistant parts from session messages.
34
41
  * Used by both /resume and /fork to show recent assistant context.
@@ -170,6 +177,73 @@ export function getToolSummaryText(part: Part): string {
170
177
  : `(+${added}-${removed})`
171
178
  }
172
179
 
180
+ if (part.tool === 'apply_patch') {
181
+ const state = part.state as {
182
+ metadata?: { files?: unknown; diff?: unknown }
183
+ output?: unknown
184
+ }
185
+ const rawFiles = state.metadata?.files
186
+ const partMetaFiles = (part as { metadata?: { files?: unknown } }).metadata?.files
187
+ const filesList = Array.isArray(rawFiles)
188
+ ? rawFiles
189
+ : Array.isArray(partMetaFiles)
190
+ ? partMetaFiles
191
+ : []
192
+
193
+ const summarizeFiles = (files: unknown[]): string => {
194
+ const summarized = files
195
+ .map((f) => {
196
+ if (!f) {
197
+ return null
198
+ }
199
+ if (typeof f === 'string') {
200
+ const fileName = f.split('/').pop() || ''
201
+ return fileName ? `*${escapeInlineMarkdown(fileName)}* (+0-0)` : `(+0-0)`
202
+ }
203
+ if (typeof f !== 'object') {
204
+ return null
205
+ }
206
+ const file = f as Record<string, unknown>
207
+ const pathStr = String(file.relativePath || file.filePath || file.path || '')
208
+ const fileName = pathStr.split('/').pop() || ''
209
+ const added = typeof file.additions === 'number' ? file.additions : 0
210
+ const removed = typeof file.deletions === 'number' ? file.deletions : 0
211
+ return fileName
212
+ ? `*${escapeInlineMarkdown(fileName)}* (+${added}-${removed})`
213
+ : `(+${added}-${removed})`
214
+ })
215
+ .filter(Boolean)
216
+ .join(', ')
217
+ return summarized
218
+ }
219
+
220
+ if (filesList.length > 0) {
221
+ const summarized = summarizeFiles(filesList)
222
+ if (summarized) {
223
+ return summarized
224
+ }
225
+ }
226
+
227
+ const outputText = typeof state.output === 'string' ? state.output : ''
228
+ const outputLines = outputText.split('\n')
229
+ const updatedIndex = outputLines.findIndex((line) =>
230
+ line.startsWith('Success. Updated the following files:'),
231
+ )
232
+ if (updatedIndex !== -1) {
233
+ const fileLines = outputLines.slice(updatedIndex + 1).filter(Boolean)
234
+ if (fileLines.length > 0) {
235
+ const summarized = summarizeFiles(
236
+ fileLines.map((line) => line.replace(/^[AMD]\s+/, '').trim()),
237
+ )
238
+ if (summarized) {
239
+ return summarized
240
+ }
241
+ }
242
+ }
243
+
244
+ return ''
245
+ }
246
+
173
247
  if (part.tool === 'write') {
174
248
  const filePath = (part.state.input?.filePath as string) || ''
175
249
  const content = (part.state.input?.content as string) || ''
@@ -228,7 +302,8 @@ export function getToolSummaryText(part: Part): string {
228
302
  .map(([key, value]) => {
229
303
  if (value === null || value === undefined) return null
230
304
  const stringValue = typeof value === 'string' ? value : JSON.stringify(value)
231
- const truncatedValue = stringValue.length > 50 ? stringValue.slice(0, 50) + '…' : stringValue
305
+ const normalized = normalizeWhitespace(stringValue)
306
+ const truncatedValue = normalized.length > 50 ? normalized.slice(0, 50) + '…' : normalized
232
307
  return `${key}: ${truncatedValue}`
233
308
  })
234
309
  .filter(Boolean)
@@ -259,7 +334,7 @@ export function formatTodoList(part: Part): string {
259
334
  }
260
335
 
261
336
  export function formatPart(part: Part, prefix?: string): string {
262
- const pfx = prefix ? `${prefix}: ` : ''
337
+ const pfx = prefix ? `${prefix} ` : ''
263
338
 
264
339
  if (part.type === 'text') {
265
340
  if (!part.text?.trim()) return ''
@@ -343,12 +418,13 @@ export function formatPart(part: Part, prefix?: string): string {
343
418
  if (part.state.status === 'error') {
344
419
  return '⨯'
345
420
  }
346
- if (part.tool === 'edit' || part.tool === 'write') {
421
+ if (part.tool === 'edit' || part.tool === 'write' || part.tool === 'apply_patch') {
347
422
  return '◼︎'
348
423
  }
349
424
  return '┣'
350
425
  })()
351
- return `${icon} ${pfx}${part.tool} ${toolTitle} ${summaryText}`.trim()
426
+ const toolParts = [part.tool, toolTitle, summaryText].filter(Boolean).join(' ')
427
+ return `${icon} ${pfx}${toolParts}`
352
428
  }
353
429
 
354
430
  logger.warn('Unknown part type:', part)
@@ -5,9 +5,9 @@
5
5
  import { RealtimeClient } from '@openai/realtime-api-beta'
6
6
  import { writeFile } from 'fs'
7
7
  import type { Tool } from 'ai'
8
- import { createLogger } from './logger.js'
8
+ import { createLogger, LogPrefix } from './logger.js'
9
9
 
10
- const openaiLogger = createLogger('OPENAI')
10
+ const openaiLogger = createLogger(LogPrefix.OPENAI)
11
11
 
12
12
  // Export the session type for reuse
13
13
  export interface OpenAIRealtimeSession {
package/src/opencode.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  type OpencodeClient as OpencodeClientV2,
13
13
  } from '@opencode-ai/sdk/v2'
14
14
  import * as errore from 'errore'
15
- import { createLogger } from './logger.js'
15
+ import { createLogger, LogPrefix } from './logger.js'
16
16
  import {
17
17
  DirectoryNotAccessibleError,
18
18
  ServerStartError,
@@ -21,7 +21,7 @@ import {
21
21
  type OpenCodeErrors,
22
22
  } from './errors.js'
23
23
 
24
- const opencodeLogger = createLogger('OPENCODE')
24
+ const opencodeLogger = createLogger(LogPrefix.OPENCODE)
25
25
 
26
26
  const opencodeServers = new Map<
27
27
  string,