kimaki 0.4.46 → 0.4.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +69 -21
- package/dist/commands/abort.js +4 -2
- package/dist/commands/add-project.js +2 -2
- package/dist/commands/agent.js +4 -4
- package/dist/commands/ask-question.js +9 -8
- package/dist/commands/compact.js +126 -0
- package/dist/commands/create-new-project.js +60 -30
- package/dist/commands/fork.js +3 -3
- package/dist/commands/merge-worktree.js +23 -10
- package/dist/commands/model.js +5 -5
- package/dist/commands/permissions.js +5 -3
- package/dist/commands/queue.js +2 -2
- package/dist/commands/remove-project.js +2 -2
- package/dist/commands/resume.js +2 -2
- package/dist/commands/session.js +6 -3
- package/dist/commands/share.js +2 -2
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/user-command.js +2 -2
- package/dist/commands/verbosity.js +5 -5
- package/dist/commands/worktree-settings.js +2 -2
- package/dist/commands/worktree.js +18 -8
- package/dist/config.js +7 -0
- package/dist/database.js +10 -7
- package/dist/discord-bot.js +30 -12
- package/dist/discord-utils.js +2 -2
- package/dist/genai-worker-wrapper.js +3 -3
- package/dist/genai-worker.js +2 -2
- package/dist/genai.js +2 -2
- package/dist/interaction-handler.js +6 -2
- package/dist/logger.js +57 -9
- package/dist/markdown.js +2 -2
- package/dist/message-formatting.js +91 -6
- package/dist/openai-realtime.js +2 -2
- package/dist/opencode.js +19 -25
- package/dist/session-handler.js +89 -29
- package/dist/system-message.js +11 -9
- package/dist/tools.js +3 -2
- package/dist/utils.js +1 -0
- package/dist/voice-handler.js +2 -2
- package/dist/voice.js +2 -2
- package/dist/worktree-utils.js +91 -7
- package/dist/xml.js +2 -2
- package/package.json +3 -3
- package/src/cli.ts +108 -21
- package/src/commands/abort.ts +4 -2
- package/src/commands/add-project.ts +2 -2
- package/src/commands/agent.ts +4 -4
- package/src/commands/ask-question.ts +9 -8
- package/src/commands/compact.ts +148 -0
- package/src/commands/create-new-project.ts +87 -36
- package/src/commands/fork.ts +3 -3
- package/src/commands/merge-worktree.ts +47 -10
- package/src/commands/model.ts +5 -5
- package/src/commands/permissions.ts +6 -2
- package/src/commands/queue.ts +2 -2
- package/src/commands/remove-project.ts +2 -2
- package/src/commands/resume.ts +2 -2
- package/src/commands/session.ts +6 -3
- package/src/commands/share.ts +2 -2
- package/src/commands/undo-redo.ts +2 -2
- package/src/commands/user-command.ts +2 -2
- package/src/commands/verbosity.ts +5 -5
- package/src/commands/worktree-settings.ts +2 -2
- package/src/commands/worktree.ts +20 -7
- package/src/config.ts +14 -0
- package/src/database.ts +13 -7
- package/src/discord-bot.ts +45 -12
- package/src/discord-utils.ts +2 -2
- package/src/genai-worker-wrapper.ts +3 -3
- package/src/genai-worker.ts +2 -2
- package/src/genai.ts +2 -2
- package/src/interaction-handler.ts +7 -2
- package/src/logger.ts +64 -10
- package/src/markdown.ts +2 -2
- package/src/message-formatting.ts +100 -6
- package/src/openai-realtime.ts +2 -2
- package/src/opencode.ts +19 -26
- package/src/session-handler.ts +102 -29
- package/src/system-message.ts +11 -9
- package/src/tools.ts +3 -2
- package/src/utils.ts +1 -0
- package/src/voice-handler.ts +2 -2
- package/src/voice.ts +2 -2
- package/src/worktree-utils.ts +111 -7
- package/src/xml.ts +2 -2
package/src/commands/worktree.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
|
|
@@ -337,6 +343,11 @@ async function handleWorktreeInThread({
|
|
|
337
343
|
return
|
|
338
344
|
}
|
|
339
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
|
+
|
|
340
351
|
// Store pending worktree in database for this existing thread
|
|
341
352
|
createPendingWorktree({
|
|
342
353
|
threadId: thread.id,
|
|
@@ -345,20 +356,22 @@ async function handleWorktreeInThread({
|
|
|
345
356
|
})
|
|
346
357
|
|
|
347
358
|
// Send status message in thread
|
|
359
|
+
const diffNote = hasDiff ? '\n📋 Will transfer uncommitted changes' : ''
|
|
348
360
|
const statusMessage = await thread.send({
|
|
349
|
-
content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up
|
|
361
|
+
content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...${diffNote}`,
|
|
350
362
|
flags: SILENT_MESSAGE_FLAGS,
|
|
351
363
|
})
|
|
352
364
|
|
|
353
365
|
await command.editReply(`Creating worktree \`${worktreeName}\` for this thread...`)
|
|
354
366
|
|
|
355
|
-
// Create worktree in background
|
|
367
|
+
// Create worktree in background, passing diff to apply after creation
|
|
356
368
|
createWorktreeInBackground({
|
|
357
369
|
thread,
|
|
358
370
|
starterMessage: statusMessage,
|
|
359
371
|
worktreeName,
|
|
360
372
|
projectDirectory,
|
|
361
373
|
clientV2,
|
|
374
|
+
diff,
|
|
362
375
|
}).catch((e) => {
|
|
363
376
|
logger.error('[NEW-WORKTREE] Background error:', e)
|
|
364
377
|
})
|
package/src/config.ts
CHANGED
|
@@ -44,6 +44,20 @@ export function getProjectsDir(): string {
|
|
|
44
44
|
return path.join(getDataDir(), 'projects')
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Default verbosity for channels that haven't set a per-channel override.
|
|
48
|
+
// Set via --verbosity CLI flag at startup.
|
|
49
|
+
import type { VerbosityLevel } from './database.js'
|
|
50
|
+
|
|
51
|
+
let defaultVerbosity: VerbosityLevel = 'tools-and-text'
|
|
52
|
+
|
|
53
|
+
export function getDefaultVerbosity(): VerbosityLevel {
|
|
54
|
+
return defaultVerbosity
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function setDefaultVerbosity(level: VerbosityLevel): void {
|
|
58
|
+
defaultVerbosity = level
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
const DEFAULT_LOCK_PORT = 29988
|
|
48
62
|
|
|
49
63
|
/**
|
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'
|
|
10
|
-
import { getDataDir } from './config.js'
|
|
9
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
10
|
+
import { getDataDir, getDefaultVerbosity } from './config.js'
|
|
11
11
|
|
|
12
|
-
const dbLogger = createLogger(
|
|
12
|
+
const dbLogger = createLogger(LogPrefix.DB)
|
|
13
13
|
|
|
14
14
|
let db: Database.Database | null = null
|
|
15
15
|
|
|
@@ -69,8 +69,11 @@ export function getDatabase(): Database.Database {
|
|
|
69
69
|
// Migration: add app_id column to channel_directories for multi-bot support
|
|
70
70
|
try {
|
|
71
71
|
db.exec(`ALTER TABLE channel_directories ADD COLUMN app_id TEXT`)
|
|
72
|
-
} catch {
|
|
73
|
-
|
|
72
|
+
} catch (error) {
|
|
73
|
+
dbLogger.debug(
|
|
74
|
+
'Failed to add app_id column to channel_directories (likely exists):',
|
|
75
|
+
error instanceof Error ? error.message : String(error),
|
|
76
|
+
)
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
// Table for threads that should auto-start a session (created by CLI without --notify-only)
|
|
@@ -378,14 +381,17 @@ export function runVerbosityMigrations(database?: Database.Database): void {
|
|
|
378
381
|
|
|
379
382
|
/**
|
|
380
383
|
* Get the verbosity setting for a channel.
|
|
381
|
-
*
|
|
384
|
+
* Falls back to the global default set via --verbosity CLI flag if no per-channel override exists.
|
|
382
385
|
*/
|
|
383
386
|
export function getChannelVerbosity(channelId: string): VerbosityLevel {
|
|
384
387
|
const db = getDatabase()
|
|
385
388
|
const row = db
|
|
386
389
|
.prepare('SELECT verbosity FROM channel_verbosity WHERE channel_id = ?')
|
|
387
390
|
.get(channelId) as { verbosity: string } | undefined
|
|
388
|
-
|
|
391
|
+
if (row?.verbosity) {
|
|
392
|
+
return row.verbosity as VerbosityLevel
|
|
393
|
+
}
|
|
394
|
+
return getDefaultVerbosity()
|
|
389
395
|
}
|
|
390
396
|
|
|
391
397
|
/**
|
package/src/discord-bot.ts
CHANGED
|
@@ -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,12 @@ 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(
|
|
78
|
-
const voiceLogger = createLogger(
|
|
77
|
+
const discordLogger = createLogger(LogPrefix.DISCORD)
|
|
78
|
+
const voiceLogger = createLogger(LogPrefix.VOICE)
|
|
79
|
+
|
|
80
|
+
function prefixWithDiscordUser({ username, prompt }: { username: string; prompt: string }): string {
|
|
81
|
+
return `<discord-user name="${username}" />\n${prompt}`
|
|
82
|
+
}
|
|
79
83
|
|
|
80
84
|
type StartOptions = {
|
|
81
85
|
token: string
|
|
@@ -165,6 +169,17 @@ export async function startDiscordBot({
|
|
|
165
169
|
if (message.author?.bot) {
|
|
166
170
|
return
|
|
167
171
|
}
|
|
172
|
+
|
|
173
|
+
// Ignore messages that start with a mention of another user (not the bot).
|
|
174
|
+
// These are likely users talking to each other, not the bot.
|
|
175
|
+
const leadingMentionMatch = message.content?.match(/^<@!?(\d+)>/)
|
|
176
|
+
if (leadingMentionMatch) {
|
|
177
|
+
const mentionedUserId = leadingMentionMatch[1]
|
|
178
|
+
if (mentionedUserId !== discordClient.user?.id) {
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
168
183
|
if (message.partial) {
|
|
169
184
|
discordLogger.log(`Fetching partial message ${message.id}`)
|
|
170
185
|
const fetched = await errore.tryAsync({
|
|
@@ -275,13 +290,19 @@ export async function startDiscordBot({
|
|
|
275
290
|
|
|
276
291
|
// Include starter message as context for the session
|
|
277
292
|
let prompt = message.content
|
|
278
|
-
const starterMessage = await thread.fetchStarterMessage().catch(() =>
|
|
293
|
+
const starterMessage = await thread.fetchStarterMessage().catch((error) => {
|
|
294
|
+
discordLogger.warn(
|
|
295
|
+
`[SESSION] Failed to fetch starter message for thread ${thread.id}:`,
|
|
296
|
+
error instanceof Error ? error.message : String(error),
|
|
297
|
+
)
|
|
298
|
+
return null
|
|
299
|
+
})
|
|
279
300
|
if (starterMessage?.content && starterMessage.content !== message.content) {
|
|
280
301
|
prompt = `Context from thread:\n${starterMessage.content}\n\nUser request:\n${message.content}`
|
|
281
302
|
}
|
|
282
303
|
|
|
283
304
|
await handleOpencodeSession({
|
|
284
|
-
prompt,
|
|
305
|
+
prompt: prefixWithDiscordUser({ username: message.member?.displayName || message.author.displayName, prompt }),
|
|
285
306
|
thread,
|
|
286
307
|
projectDirectory,
|
|
287
308
|
channelId: parent?.id || '',
|
|
@@ -358,7 +379,7 @@ export async function startDiscordBot({
|
|
|
358
379
|
? `${messageContent}\n\n${textAttachmentsContent}`
|
|
359
380
|
: messageContent
|
|
360
381
|
await handleOpencodeSession({
|
|
361
|
-
prompt: promptWithAttachments,
|
|
382
|
+
prompt: prefixWithDiscordUser({ username: message.member?.displayName || message.author.displayName, prompt: promptWithAttachments }),
|
|
362
383
|
thread,
|
|
363
384
|
projectDirectory,
|
|
364
385
|
originalMessage: message,
|
|
@@ -502,7 +523,7 @@ export async function startDiscordBot({
|
|
|
502
523
|
? `${messageContent}\n\n${textAttachmentsContent}`
|
|
503
524
|
: messageContent
|
|
504
525
|
await handleOpencodeSession({
|
|
505
|
-
prompt: promptWithAttachments,
|
|
526
|
+
prompt: prefixWithDiscordUser({ username: message.member?.displayName || message.author.displayName, prompt: promptWithAttachments }),
|
|
506
527
|
thread,
|
|
507
528
|
projectDirectory: sessionDirectory,
|
|
508
529
|
originalMessage: message,
|
|
@@ -517,8 +538,11 @@ export async function startDiscordBot({
|
|
|
517
538
|
try {
|
|
518
539
|
const errMsg = error instanceof Error ? error.message : String(error)
|
|
519
540
|
await message.reply({ content: `Error: ${errMsg}`, flags: SILENT_MESSAGE_FLAGS })
|
|
520
|
-
} catch {
|
|
521
|
-
voiceLogger.error(
|
|
541
|
+
} catch (sendError) {
|
|
542
|
+
voiceLogger.error(
|
|
543
|
+
'Discord handler error (fallback):',
|
|
544
|
+
sendError instanceof Error ? sendError.message : String(sendError),
|
|
545
|
+
)
|
|
522
546
|
}
|
|
523
547
|
}
|
|
524
548
|
})
|
|
@@ -539,7 +563,13 @@ export async function startDiscordBot({
|
|
|
539
563
|
}
|
|
540
564
|
|
|
541
565
|
// Get the starter message to check for auto-start marker
|
|
542
|
-
const starterMessage = await thread.fetchStarterMessage().catch(() =>
|
|
566
|
+
const starterMessage = await thread.fetchStarterMessage().catch((error) => {
|
|
567
|
+
discordLogger.warn(
|
|
568
|
+
`[THREAD_CREATE] Failed to fetch starter message for thread ${thread.id}:`,
|
|
569
|
+
error instanceof Error ? error.message : String(error),
|
|
570
|
+
)
|
|
571
|
+
return null
|
|
572
|
+
})
|
|
543
573
|
if (!starterMessage) {
|
|
544
574
|
discordLogger.log(`[THREAD_CREATE] Could not fetch starter message for thread ${thread.id}`)
|
|
545
575
|
return
|
|
@@ -601,8 +631,11 @@ export async function startDiscordBot({
|
|
|
601
631
|
try {
|
|
602
632
|
const errMsg = error instanceof Error ? error.message : String(error)
|
|
603
633
|
await thread.send({ content: `Error: ${errMsg}`, flags: SILENT_MESSAGE_FLAGS })
|
|
604
|
-
} catch {
|
|
605
|
-
|
|
634
|
+
} catch (sendError) {
|
|
635
|
+
voiceLogger.error(
|
|
636
|
+
'[BOT_SESSION] Failed to send error message:',
|
|
637
|
+
sendError instanceof Error ? sendError.message : String(sendError),
|
|
638
|
+
)
|
|
606
639
|
}
|
|
607
640
|
}
|
|
608
641
|
})
|
package/src/discord-utils.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
11
|
-
const genaiWrapperLogger = createLogger(
|
|
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
|
package/src/genai-worker.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
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(
|
|
22
|
+
const logger = createLogger(LogPrefix.FORMATTING)
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Escapes Discord inline markdown characters so dynamic content
|
|
@@ -29,6 +29,81 @@ function escapeInlineMarkdown(text: string): string {
|
|
|
29
29
|
return text.replace(/([*_~|`\\])/g, '\\$1')
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Parses a patchText string (apply_patch format) and counts additions/deletions per file.
|
|
34
|
+
* Patch format uses `*** Add File:`, `*** Update File:`, `*** Delete File:` headers,
|
|
35
|
+
* with diff lines prefixed by `+` (addition) or `-` (deletion) inside `@@` hunks.
|
|
36
|
+
*/
|
|
37
|
+
function parsePatchCounts(
|
|
38
|
+
patchText: string,
|
|
39
|
+
): Map<string, { additions: number; deletions: number }> {
|
|
40
|
+
const counts = new Map<string, { additions: number; deletions: number }>()
|
|
41
|
+
const lines = patchText.split('\n')
|
|
42
|
+
let currentFile = ''
|
|
43
|
+
let currentType = ''
|
|
44
|
+
let inHunk = false
|
|
45
|
+
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
const addMatch = line.match(/^\*\*\* Add File:\s*(.+)/)
|
|
48
|
+
const updateMatch = line.match(/^\*\*\* Update File:\s*(.+)/)
|
|
49
|
+
const deleteMatch = line.match(/^\*\*\* Delete File:\s*(.+)/)
|
|
50
|
+
|
|
51
|
+
if (addMatch || updateMatch || deleteMatch) {
|
|
52
|
+
const match = addMatch || updateMatch || deleteMatch
|
|
53
|
+
currentFile = (match?.[1] ?? '').trim()
|
|
54
|
+
currentType = addMatch ? 'add' : updateMatch ? 'update' : 'delete'
|
|
55
|
+
counts.set(currentFile, { additions: 0, deletions: 0 })
|
|
56
|
+
inHunk = false
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (line.startsWith('@@')) {
|
|
61
|
+
inHunk = true
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (line.startsWith('*** ')) {
|
|
66
|
+
inHunk = false
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!currentFile) {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const entry = counts.get(currentFile)
|
|
75
|
+
if (!entry) {
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (currentType === 'add') {
|
|
80
|
+
// all content lines in Add File are additions
|
|
81
|
+
if (line.length > 0 && !line.startsWith('*** ')) {
|
|
82
|
+
entry.additions++
|
|
83
|
+
}
|
|
84
|
+
} else if (currentType === 'delete') {
|
|
85
|
+
// all content lines in Delete File are deletions
|
|
86
|
+
if (line.length > 0 && !line.startsWith('*** ')) {
|
|
87
|
+
entry.deletions++
|
|
88
|
+
}
|
|
89
|
+
} else if (inHunk) {
|
|
90
|
+
if (line.startsWith('+')) {
|
|
91
|
+
entry.additions++
|
|
92
|
+
} else if (line.startsWith('-')) {
|
|
93
|
+
entry.deletions++
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return counts
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Normalize whitespace: convert newlines to spaces and collapse consecutive spaces.
|
|
102
|
+
*/
|
|
103
|
+
function normalizeWhitespace(text: string): string {
|
|
104
|
+
return text.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ')
|
|
105
|
+
}
|
|
106
|
+
|
|
32
107
|
/**
|
|
33
108
|
* Collects and formats the last N assistant parts from session messages.
|
|
34
109
|
* Used by both /resume and /fork to show recent assistant context.
|
|
@@ -170,6 +245,23 @@ export function getToolSummaryText(part: Part): string {
|
|
|
170
245
|
: `(+${added}-${removed})`
|
|
171
246
|
}
|
|
172
247
|
|
|
248
|
+
if (part.tool === 'apply_patch') {
|
|
249
|
+
// Only inputs are available when parts are sent during streaming (output/metadata not yet populated)
|
|
250
|
+
const patchText = (part.state.input?.patchText as string) || ''
|
|
251
|
+
if (!patchText) {
|
|
252
|
+
return ''
|
|
253
|
+
}
|
|
254
|
+
const patchCounts = parsePatchCounts(patchText)
|
|
255
|
+
return [...patchCounts.entries()]
|
|
256
|
+
.map(([filePath, { additions, deletions }]) => {
|
|
257
|
+
const fileName = filePath.split('/').pop() || ''
|
|
258
|
+
return fileName
|
|
259
|
+
? `*${escapeInlineMarkdown(fileName)}* (+${additions}-${deletions})`
|
|
260
|
+
: `(+${additions}-${deletions})`
|
|
261
|
+
})
|
|
262
|
+
.join(', ')
|
|
263
|
+
}
|
|
264
|
+
|
|
173
265
|
if (part.tool === 'write') {
|
|
174
266
|
const filePath = (part.state.input?.filePath as string) || ''
|
|
175
267
|
const content = (part.state.input?.content as string) || ''
|
|
@@ -228,7 +320,8 @@ export function getToolSummaryText(part: Part): string {
|
|
|
228
320
|
.map(([key, value]) => {
|
|
229
321
|
if (value === null || value === undefined) return null
|
|
230
322
|
const stringValue = typeof value === 'string' ? value : JSON.stringify(value)
|
|
231
|
-
const
|
|
323
|
+
const normalized = normalizeWhitespace(stringValue)
|
|
324
|
+
const truncatedValue = normalized.length > 50 ? normalized.slice(0, 50) + '…' : normalized
|
|
232
325
|
return `${key}: ${truncatedValue}`
|
|
233
326
|
})
|
|
234
327
|
.filter(Boolean)
|
|
@@ -259,7 +352,7 @@ export function formatTodoList(part: Part): string {
|
|
|
259
352
|
}
|
|
260
353
|
|
|
261
354
|
export function formatPart(part: Part, prefix?: string): string {
|
|
262
|
-
const pfx = prefix ? `${prefix}
|
|
355
|
+
const pfx = prefix ? `${prefix} ⋅ ` : ''
|
|
263
356
|
|
|
264
357
|
if (part.type === 'text') {
|
|
265
358
|
if (!part.text?.trim()) return ''
|
|
@@ -343,12 +436,13 @@ export function formatPart(part: Part, prefix?: string): string {
|
|
|
343
436
|
if (part.state.status === 'error') {
|
|
344
437
|
return '⨯'
|
|
345
438
|
}
|
|
346
|
-
if (part.tool === 'edit' || part.tool === 'write') {
|
|
439
|
+
if (part.tool === 'edit' || part.tool === 'write' || part.tool === 'apply_patch') {
|
|
347
440
|
return '◼︎'
|
|
348
441
|
}
|
|
349
442
|
return '┣'
|
|
350
443
|
})()
|
|
351
|
-
|
|
444
|
+
const toolParts = [part.tool, toolTitle, summaryText].filter(Boolean).join(' ')
|
|
445
|
+
return `${icon} ${pfx}${toolParts}`
|
|
352
446
|
}
|
|
353
447
|
|
|
354
448
|
logger.warn('Unknown part type:', part)
|