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.
- package/dist/cli.js +27 -2
- package/dist/commands/abort.js +2 -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 +5 -3
- package/dist/commands/fork.js +5 -3
- package/dist/commands/merge-worktree.js +2 -2
- package/dist/commands/model.js +5 -5
- package/dist/commands/permissions.js +2 -2
- package/dist/commands/queue.js +2 -2
- package/dist/commands/remove-project.js +2 -2
- package/dist/commands/resume.js +4 -2
- package/dist/commands/session.js +4 -2
- package/dist/commands/share.js +2 -2
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/user-command.js +4 -2
- package/dist/commands/verbosity.js +3 -3
- package/dist/commands/worktree-settings.js +2 -2
- package/dist/commands/worktree.js +20 -8
- package/dist/database.js +2 -2
- package/dist/discord-bot.js +5 -3
- 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 +69 -6
- package/dist/openai-realtime.js +2 -2
- package/dist/opencode.js +2 -2
- package/dist/session-handler.js +93 -15
- package/dist/tools.js +2 -2
- 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 +1 -1
- package/src/cli.ts +28 -2
- package/src/commands/abort.ts +2 -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 +6 -3
- package/src/commands/fork.ts +6 -3
- package/src/commands/merge-worktree.ts +2 -2
- package/src/commands/model.ts +5 -5
- package/src/commands/permissions.ts +2 -2
- package/src/commands/queue.ts +2 -2
- package/src/commands/remove-project.ts +2 -2
- package/src/commands/resume.ts +5 -2
- package/src/commands/session.ts +5 -2
- package/src/commands/share.ts +2 -2
- package/src/commands/undo-redo.ts +2 -2
- package/src/commands/user-command.ts +5 -2
- package/src/commands/verbosity.ts +3 -3
- package/src/commands/worktree-settings.ts +2 -2
- package/src/commands/worktree.ts +23 -7
- package/src/database.ts +2 -2
- package/src/discord-bot.ts +6 -3
- 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 +82 -6
- package/src/openai-realtime.ts +2 -2
- package/src/opencode.ts +2 -2
- package/src/session-handler.ts +105 -15
- package/src/tools.ts +2 -2
- 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/share.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
10
|
+
const worktreeSettingsLogger = createLogger(LogPrefix.WORKTREE)
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Handle the /enable-worktrees slash command.
|
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
|
|
|
@@ -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(
|
|
12
|
+
const dbLogger = createLogger(LogPrefix.DB)
|
|
13
13
|
|
|
14
14
|
let db: Database.Database | null = null
|
|
15
15
|
|
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,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(
|
|
78
|
-
const voiceLogger = createLogger(
|
|
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)
|
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,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
|
|
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
|
-
|
|
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)
|
package/src/openai-realtime.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
24
|
+
const opencodeLogger = createLogger(LogPrefix.OPENCODE)
|
|
25
25
|
|
|
26
26
|
const opencodeServers = new Map<
|
|
27
27
|
string,
|