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/dist/database.js
CHANGED
|
@@ -5,9 +5,9 @@ import Database from 'better-sqlite3';
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import * as errore from 'errore';
|
|
8
|
-
import { createLogger } from './logger.js';
|
|
8
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
9
9
|
import { getDataDir } from './config.js';
|
|
10
|
-
const dbLogger = createLogger(
|
|
10
|
+
const dbLogger = createLogger(LogPrefix.DB);
|
|
11
11
|
let db = null;
|
|
12
12
|
export function getDatabase() {
|
|
13
13
|
if (!db) {
|
package/dist/discord-bot.js
CHANGED
|
@@ -22,14 +22,14 @@ export { ensureKimakiCategory, ensureKimakiAudioCategory, createProjectChannels,
|
|
|
22
22
|
import { ChannelType, Client, Events, GatewayIntentBits, Partials, PermissionsBitField, ThreadAutoArchiveDuration, } from 'discord.js';
|
|
23
23
|
import fs from 'node:fs';
|
|
24
24
|
import * as errore from 'errore';
|
|
25
|
-
import { createLogger } from './logger.js';
|
|
25
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
26
26
|
import { setGlobalDispatcher, Agent } from 'undici';
|
|
27
27
|
// Increase connection pool to prevent deadlock when multiple sessions have open SSE streams.
|
|
28
28
|
// Each session's event.subscribe() holds a connection; without enough connections,
|
|
29
29
|
// regular HTTP requests (question.reply, session.prompt) get blocked → deadlock.
|
|
30
30
|
setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0, connections: 500 }));
|
|
31
|
-
const discordLogger = createLogger(
|
|
32
|
-
const voiceLogger = createLogger(
|
|
31
|
+
const discordLogger = createLogger(LogPrefix.DISCORD);
|
|
32
|
+
const voiceLogger = createLogger(LogPrefix.VOICE);
|
|
33
33
|
export async function createDiscordClient() {
|
|
34
34
|
return new Client({
|
|
35
35
|
intents: [
|
|
@@ -309,6 +309,8 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
309
309
|
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
310
310
|
reason: 'Start Claude session',
|
|
311
311
|
});
|
|
312
|
+
// Add user to thread so it appears in their sidebar
|
|
313
|
+
await thread.members.add(message.author.id);
|
|
312
314
|
discordLogger.log(`Created thread "${thread.name}" (${thread.id})`);
|
|
313
315
|
// Create worktree if worktrees are enabled (CLI flag OR channel setting)
|
|
314
316
|
let sessionDirectory = projectDirectory;
|
package/dist/discord-utils.js
CHANGED
|
@@ -7,11 +7,11 @@ import { formatMarkdownTables } from './format-tables.js';
|
|
|
7
7
|
import { getChannelDirectory } from './database.js';
|
|
8
8
|
import { limitHeadingDepth } from './limit-heading-depth.js';
|
|
9
9
|
import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js';
|
|
10
|
-
import { createLogger } from './logger.js';
|
|
10
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
11
11
|
import mime from 'mime';
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
-
const discordLogger = createLogger(
|
|
14
|
+
const discordLogger = createLogger(LogPrefix.DISCORD);
|
|
15
15
|
export const SILENT_MESSAGE_FLAGS = 4 | 4096;
|
|
16
16
|
// Same as SILENT but without SuppressNotifications - triggers badge/notification
|
|
17
17
|
export const NOTIFY_MESSAGE_FLAGS = 4;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Spawns and manages the worker thread, handling message passing for
|
|
3
3
|
// audio input/output, tool call completions, and graceful shutdown.
|
|
4
4
|
import { Worker } from 'node:worker_threads';
|
|
5
|
-
import { createLogger } from './logger.js';
|
|
6
|
-
const genaiWorkerLogger = createLogger(
|
|
7
|
-
const genaiWrapperLogger = createLogger(
|
|
5
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
6
|
+
const genaiWorkerLogger = createLogger(LogPrefix.GENAI_WORKER);
|
|
7
|
+
const genaiWrapperLogger = createLogger(LogPrefix.GENAI_WORKER);
|
|
8
8
|
export function createGenAIWorker(options) {
|
|
9
9
|
return new Promise((resolve, reject) => {
|
|
10
10
|
const worker = new Worker(new URL('../dist/genai-worker.js', import.meta.url));
|
package/dist/genai-worker.js
CHANGED
|
@@ -10,11 +10,11 @@ import * as prism from 'prism-media';
|
|
|
10
10
|
import { startGenAiSession } from './genai.js';
|
|
11
11
|
import { getTools } from './tools.js';
|
|
12
12
|
import { mkdir } from 'node:fs/promises';
|
|
13
|
-
import { createLogger } from './logger.js';
|
|
13
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
14
14
|
if (!parentPort) {
|
|
15
15
|
throw new Error('This module must be run as a worker thread');
|
|
16
16
|
}
|
|
17
|
-
const workerLogger = createLogger(
|
|
17
|
+
const workerLogger = createLogger(`${LogPrefix.WORKER}_${threadId}`);
|
|
18
18
|
workerLogger.log('GenAI worker started');
|
|
19
19
|
// Define sendError early so it can be used by global handlers
|
|
20
20
|
function sendError(error) {
|
package/dist/genai.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// and manages the assistant's audio output for Discord voice channels.
|
|
4
4
|
import { GoogleGenAI, LiveServerMessage, MediaResolution, Modality, Session } from '@google/genai';
|
|
5
5
|
import { writeFile } from 'fs';
|
|
6
|
-
import { createLogger } from './logger.js';
|
|
6
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
7
7
|
import { aiToolToCallableTool } from './ai-tool-to-genai.js';
|
|
8
|
-
const genaiLogger = createLogger(
|
|
8
|
+
const genaiLogger = createLogger(LogPrefix.GENAI);
|
|
9
9
|
const audioParts = [];
|
|
10
10
|
function saveBinaryFile(fileName, content) {
|
|
11
11
|
writeFile(fileName, content, 'utf8', (err) => {
|
|
@@ -12,6 +12,7 @@ import { handleRemoveProjectCommand, handleRemoveProjectAutocomplete, } from './
|
|
|
12
12
|
import { handleCreateNewProjectCommand } from './commands/create-new-project.js';
|
|
13
13
|
import { handlePermissionSelectMenu } from './commands/permissions.js';
|
|
14
14
|
import { handleAbortCommand } from './commands/abort.js';
|
|
15
|
+
import { handleCompactCommand } from './commands/compact.js';
|
|
15
16
|
import { handleShareCommand } from './commands/share.js';
|
|
16
17
|
import { handleForkCommand, handleForkSelectMenu } from './commands/fork.js';
|
|
17
18
|
import { handleModelCommand, handleProviderSelectMenu, handleModelSelectMenu, } from './commands/model.js';
|
|
@@ -21,8 +22,8 @@ import { handleQueueCommand, handleClearQueueCommand } from './commands/queue.js
|
|
|
21
22
|
import { handleUndoCommand, handleRedoCommand } from './commands/undo-redo.js';
|
|
22
23
|
import { handleUserCommand } from './commands/user-command.js';
|
|
23
24
|
import { handleVerbosityCommand } from './commands/verbosity.js';
|
|
24
|
-
import { createLogger } from './logger.js';
|
|
25
|
-
const interactionLogger = createLogger(
|
|
25
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
26
|
+
const interactionLogger = createLogger(LogPrefix.INTERACTION);
|
|
26
27
|
export function registerInteractionHandler({ discordClient, appId, }) {
|
|
27
28
|
interactionLogger.log('[REGISTER] Interaction handler registered');
|
|
28
29
|
discordClient.on(Events.InteractionCreate, async (interaction) => {
|
|
@@ -85,6 +86,9 @@ export function registerInteractionHandler({ discordClient, appId, }) {
|
|
|
85
86
|
case 'stop':
|
|
86
87
|
await handleAbortCommand({ command: interaction, appId });
|
|
87
88
|
return;
|
|
89
|
+
case 'compact':
|
|
90
|
+
await handleCompactCommand({ command: interaction, appId });
|
|
91
|
+
return;
|
|
88
92
|
case 'share':
|
|
89
93
|
await handleShareCommand({ command: interaction, appId });
|
|
90
94
|
return;
|
package/dist/logger.js
CHANGED
|
@@ -1,11 +1,49 @@
|
|
|
1
|
-
// Prefixed logging utility
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
import { log } from '@clack/prompts';
|
|
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
|
+
import { log as clackLog } from '@clack/prompts';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path, { dirname } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import util from 'node:util';
|
|
9
|
+
import pc from 'picocolors';
|
|
10
|
+
// All known log prefixes - add new ones here to keep alignment consistent
|
|
11
|
+
export const LogPrefix = {
|
|
12
|
+
ABORT: 'ABORT',
|
|
13
|
+
ADD_PROJECT: 'ADD_PROJ',
|
|
14
|
+
AGENT: 'AGENT',
|
|
15
|
+
ASK_QUESTION: 'QUESTION',
|
|
16
|
+
CLI: 'CLI',
|
|
17
|
+
COMPACT: 'COMPACT',
|
|
18
|
+
CREATE_PROJECT: 'NEW_PROJ',
|
|
19
|
+
DB: 'DB',
|
|
20
|
+
DISCORD: 'DISCORD',
|
|
21
|
+
FORK: 'FORK',
|
|
22
|
+
FORMATTING: 'FORMAT',
|
|
23
|
+
GENAI: 'GENAI',
|
|
24
|
+
GENAI_WORKER: 'GENAI_W',
|
|
25
|
+
INTERACTION: 'INTERACT',
|
|
26
|
+
MARKDOWN: 'MARKDOWN',
|
|
27
|
+
MODEL: 'MODEL',
|
|
28
|
+
OPENAI: 'OPENAI',
|
|
29
|
+
OPENCODE: 'OPENCODE',
|
|
30
|
+
PERMISSIONS: 'PERMS',
|
|
31
|
+
QUEUE: 'QUEUE',
|
|
32
|
+
REMOVE_PROJECT: 'RM_PROJ',
|
|
33
|
+
RESUME: 'RESUME',
|
|
34
|
+
SESSION: 'SESSION',
|
|
35
|
+
SHARE: 'SHARE',
|
|
36
|
+
TOOLS: 'TOOLS',
|
|
37
|
+
UNDO_REDO: 'UNDO',
|
|
38
|
+
USER_CMD: 'USER_CMD',
|
|
39
|
+
VERBOSITY: 'VERBOSE',
|
|
40
|
+
VOICE: 'VOICE',
|
|
41
|
+
WORKER: 'WORKER',
|
|
42
|
+
WORKTREE: 'WORKTREE',
|
|
43
|
+
XML: 'XML',
|
|
44
|
+
};
|
|
45
|
+
// compute max length from all known prefixes for alignment
|
|
46
|
+
const MAX_PREFIX_LENGTH = Math.max(...Object.values(LogPrefix).map((p) => p.length));
|
|
9
47
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
48
|
const __dirname = dirname(__filename);
|
|
11
49
|
const isDev = !__dirname.includes('node_modules');
|
|
@@ -32,27 +70,37 @@ function writeToFile(level, prefix, args) {
|
|
|
32
70
|
const message = `[${timestamp}] [${level}] [${prefix}] ${args.map(formatArg).join(' ')}\n`;
|
|
33
71
|
fs.appendFileSync(logFilePath, message);
|
|
34
72
|
}
|
|
73
|
+
function getTimestamp() {
|
|
74
|
+
const now = new Date();
|
|
75
|
+
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
76
|
+
}
|
|
77
|
+
function padPrefix(prefix) {
|
|
78
|
+
return prefix.padEnd(MAX_PREFIX_LENGTH);
|
|
79
|
+
}
|
|
35
80
|
export function createLogger(prefix) {
|
|
81
|
+
const paddedPrefix = padPrefix(prefix);
|
|
36
82
|
return {
|
|
37
83
|
log: (...args) => {
|
|
38
84
|
writeToFile('INFO', prefix, args);
|
|
39
|
-
log.
|
|
85
|
+
console.log(pc.dim(getTimestamp()), pc.cyan(paddedPrefix), ...args.map(formatArg));
|
|
40
86
|
},
|
|
41
87
|
error: (...args) => {
|
|
42
88
|
writeToFile('ERROR', prefix, args);
|
|
43
|
-
|
|
89
|
+
// use clack for errors - visually distinct
|
|
90
|
+
clackLog.error([paddedPrefix, ...args.map(formatArg)].join(' '));
|
|
44
91
|
},
|
|
45
92
|
warn: (...args) => {
|
|
46
93
|
writeToFile('WARN', prefix, args);
|
|
47
|
-
|
|
94
|
+
// use clack for warnings - visually distinct
|
|
95
|
+
clackLog.warn([paddedPrefix, ...args.map(formatArg)].join(' '));
|
|
48
96
|
},
|
|
49
97
|
info: (...args) => {
|
|
50
98
|
writeToFile('INFO', prefix, args);
|
|
51
|
-
log.
|
|
99
|
+
console.log(pc.dim(getTimestamp()), pc.blue(paddedPrefix), ...args.map(formatArg));
|
|
52
100
|
},
|
|
53
101
|
debug: (...args) => {
|
|
54
102
|
writeToFile('DEBUG', prefix, args);
|
|
55
|
-
log.
|
|
103
|
+
console.log(pc.dim(getTimestamp()), pc.dim(paddedPrefix), ...args.map(formatArg));
|
|
56
104
|
},
|
|
57
105
|
};
|
|
58
106
|
}
|
package/dist/markdown.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createTaggedError } from 'errore';
|
|
|
7
7
|
import * as yaml from 'js-yaml';
|
|
8
8
|
import { formatDateTime } from './utils.js';
|
|
9
9
|
import { extractNonXmlContent } from './xml.js';
|
|
10
|
-
import { createLogger } from './logger.js';
|
|
10
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
11
11
|
import { SessionNotFoundError, MessagesNotFoundError } from './errors.js';
|
|
12
12
|
// Generic error for unexpected exceptions in async operations
|
|
13
13
|
class UnexpectedError extends createTaggedError({
|
|
@@ -15,7 +15,7 @@ class UnexpectedError extends createTaggedError({
|
|
|
15
15
|
message: '$message',
|
|
16
16
|
}) {
|
|
17
17
|
}
|
|
18
|
-
const markdownLogger = createLogger(
|
|
18
|
+
const markdownLogger = createLogger(LogPrefix.MARKDOWN);
|
|
19
19
|
export class ShareMarkdown {
|
|
20
20
|
client;
|
|
21
21
|
constructor(client) {
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import * as errore from 'errore';
|
|
7
|
-
import { createLogger } from './logger.js';
|
|
7
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
8
8
|
import { FetchError } from './errors.js';
|
|
9
9
|
const ATTACHMENTS_DIR = path.join(process.cwd(), 'tmp', 'discord-attachments');
|
|
10
|
-
const logger = createLogger(
|
|
10
|
+
const logger = createLogger(LogPrefix.FORMATTING);
|
|
11
11
|
/**
|
|
12
12
|
* Escapes Discord inline markdown characters so dynamic content
|
|
13
13
|
* doesn't break formatting when wrapped in *, _, **, etc.
|
|
@@ -15,6 +15,12 @@ const logger = createLogger('FORMATTING');
|
|
|
15
15
|
function escapeInlineMarkdown(text) {
|
|
16
16
|
return text.replace(/([*_~|`\\])/g, '\\$1');
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Normalize whitespace: convert newlines to spaces and collapse consecutive spaces.
|
|
20
|
+
*/
|
|
21
|
+
function normalizeWhitespace(text) {
|
|
22
|
+
return text.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ');
|
|
23
|
+
}
|
|
18
24
|
/**
|
|
19
25
|
* Collects and formats the last N assistant parts from session messages.
|
|
20
26
|
* Used by both /resume and /fork to show recent assistant context.
|
|
@@ -125,6 +131,61 @@ export function getToolSummaryText(part) {
|
|
|
125
131
|
? `*${escapeInlineMarkdown(fileName)}* (+${added}-${removed})`
|
|
126
132
|
: `(+${added}-${removed})`;
|
|
127
133
|
}
|
|
134
|
+
if (part.tool === 'apply_patch') {
|
|
135
|
+
const state = part.state;
|
|
136
|
+
const rawFiles = state.metadata?.files;
|
|
137
|
+
const partMetaFiles = part.metadata?.files;
|
|
138
|
+
const filesList = Array.isArray(rawFiles)
|
|
139
|
+
? rawFiles
|
|
140
|
+
: Array.isArray(partMetaFiles)
|
|
141
|
+
? partMetaFiles
|
|
142
|
+
: [];
|
|
143
|
+
const summarizeFiles = (files) => {
|
|
144
|
+
const summarized = files
|
|
145
|
+
.map((f) => {
|
|
146
|
+
if (!f) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
if (typeof f === 'string') {
|
|
150
|
+
const fileName = f.split('/').pop() || '';
|
|
151
|
+
return fileName ? `*${escapeInlineMarkdown(fileName)}* (+0-0)` : `(+0-0)`;
|
|
152
|
+
}
|
|
153
|
+
if (typeof f !== 'object') {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const file = f;
|
|
157
|
+
const pathStr = String(file.relativePath || file.filePath || file.path || '');
|
|
158
|
+
const fileName = pathStr.split('/').pop() || '';
|
|
159
|
+
const added = typeof file.additions === 'number' ? file.additions : 0;
|
|
160
|
+
const removed = typeof file.deletions === 'number' ? file.deletions : 0;
|
|
161
|
+
return fileName
|
|
162
|
+
? `*${escapeInlineMarkdown(fileName)}* (+${added}-${removed})`
|
|
163
|
+
: `(+${added}-${removed})`;
|
|
164
|
+
})
|
|
165
|
+
.filter(Boolean)
|
|
166
|
+
.join(', ');
|
|
167
|
+
return summarized;
|
|
168
|
+
};
|
|
169
|
+
if (filesList.length > 0) {
|
|
170
|
+
const summarized = summarizeFiles(filesList);
|
|
171
|
+
if (summarized) {
|
|
172
|
+
return summarized;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const outputText = typeof state.output === 'string' ? state.output : '';
|
|
176
|
+
const outputLines = outputText.split('\n');
|
|
177
|
+
const updatedIndex = outputLines.findIndex((line) => line.startsWith('Success. Updated the following files:'));
|
|
178
|
+
if (updatedIndex !== -1) {
|
|
179
|
+
const fileLines = outputLines.slice(updatedIndex + 1).filter(Boolean);
|
|
180
|
+
if (fileLines.length > 0) {
|
|
181
|
+
const summarized = summarizeFiles(fileLines.map((line) => line.replace(/^[AMD]\s+/, '').trim()));
|
|
182
|
+
if (summarized) {
|
|
183
|
+
return summarized;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return '';
|
|
188
|
+
}
|
|
128
189
|
if (part.tool === 'write') {
|
|
129
190
|
const filePath = part.state.input?.filePath || '';
|
|
130
191
|
const content = part.state.input?.content || '';
|
|
@@ -175,7 +236,8 @@ export function getToolSummaryText(part) {
|
|
|
175
236
|
if (value === null || value === undefined)
|
|
176
237
|
return null;
|
|
177
238
|
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
178
|
-
const
|
|
239
|
+
const normalized = normalizeWhitespace(stringValue);
|
|
240
|
+
const truncatedValue = normalized.length > 50 ? normalized.slice(0, 50) + '…' : normalized;
|
|
179
241
|
return `${key}: ${truncatedValue}`;
|
|
180
242
|
})
|
|
181
243
|
.filter(Boolean);
|
|
@@ -201,7 +263,7 @@ export function formatTodoList(part) {
|
|
|
201
263
|
return `${num} **${escapeInlineMarkdown(content)}**`;
|
|
202
264
|
}
|
|
203
265
|
export function formatPart(part, prefix) {
|
|
204
|
-
const pfx = prefix ? `${prefix}
|
|
266
|
+
const pfx = prefix ? `${prefix} ⋅ ` : '';
|
|
205
267
|
if (part.type === 'text') {
|
|
206
268
|
if (!part.text?.trim())
|
|
207
269
|
return '';
|
|
@@ -278,12 +340,13 @@ export function formatPart(part, prefix) {
|
|
|
278
340
|
if (part.state.status === 'error') {
|
|
279
341
|
return '⨯';
|
|
280
342
|
}
|
|
281
|
-
if (part.tool === 'edit' || part.tool === 'write') {
|
|
343
|
+
if (part.tool === 'edit' || part.tool === 'write' || part.tool === 'apply_patch') {
|
|
282
344
|
return '◼︎';
|
|
283
345
|
}
|
|
284
346
|
return '┣';
|
|
285
347
|
})();
|
|
286
|
-
|
|
348
|
+
const toolParts = [part.tool, toolTitle, summaryText].filter(Boolean).join(' ');
|
|
349
|
+
return `${icon} ${pfx}${toolParts}`;
|
|
287
350
|
}
|
|
288
351
|
logger.warn('Unknown part type:', part);
|
|
289
352
|
return '';
|
package/dist/openai-realtime.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// @ts-nocheck
|
|
4
4
|
import { RealtimeClient } from '@openai/realtime-api-beta';
|
|
5
5
|
import { writeFile } from 'fs';
|
|
6
|
-
import { createLogger } from './logger.js';
|
|
7
|
-
const openaiLogger = createLogger(
|
|
6
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
7
|
+
const openaiLogger = createLogger(LogPrefix.OPENAI);
|
|
8
8
|
const audioParts = [];
|
|
9
9
|
function saveBinaryFile(fileName, content) {
|
|
10
10
|
writeFile(fileName, content, 'utf8', (err) => {
|
package/dist/opencode.js
CHANGED
|
@@ -8,9 +8,9 @@ import net from 'node:net';
|
|
|
8
8
|
import { createOpencodeClient } from '@opencode-ai/sdk';
|
|
9
9
|
import { createOpencodeClient as createOpencodeClientV2, } from '@opencode-ai/sdk/v2';
|
|
10
10
|
import * as errore from 'errore';
|
|
11
|
-
import { createLogger } from './logger.js';
|
|
11
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
12
12
|
import { DirectoryNotAccessibleError, ServerStartError, ServerNotReadyError, FetchError, } from './errors.js';
|
|
13
|
-
const opencodeLogger = createLogger(
|
|
13
|
+
const opencodeLogger = createLogger(LogPrefix.OPENCODE);
|
|
14
14
|
const opencodeServers = new Map();
|
|
15
15
|
const serverRetryCount = new Map();
|
|
16
16
|
async function getOpenPort() {
|
package/dist/session-handler.js
CHANGED
|
@@ -7,14 +7,14 @@ import { initializeOpencodeForDirectory, getOpencodeServers, getOpencodeClientV2
|
|
|
7
7
|
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from './discord-utils.js';
|
|
8
8
|
import { formatPart } from './message-formatting.js';
|
|
9
9
|
import { getOpencodeSystemMessage } from './system-message.js';
|
|
10
|
-
import { createLogger } from './logger.js';
|
|
10
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
11
11
|
import { isAbortError } from './utils.js';
|
|
12
12
|
import { showAskUserQuestionDropdowns, cancelPendingQuestion, pendingQuestionContexts, } from './commands/ask-question.js';
|
|
13
13
|
import { showPermissionDropdown, cleanupPermissionContext, addPermissionRequestToContext, } from './commands/permissions.js';
|
|
14
14
|
import * as errore from 'errore';
|
|
15
|
-
const sessionLogger = createLogger(
|
|
16
|
-
const voiceLogger = createLogger(
|
|
17
|
-
const discordLogger = createLogger(
|
|
15
|
+
const sessionLogger = createLogger(LogPrefix.SESSION);
|
|
16
|
+
const voiceLogger = createLogger(LogPrefix.VOICE);
|
|
17
|
+
const discordLogger = createLogger(LogPrefix.DISCORD);
|
|
18
18
|
export const abortControllers = new Map();
|
|
19
19
|
// Track multiple pending permissions per thread (keyed by permission ID)
|
|
20
20
|
// OpenCode handles blocking/sequencing - we just need to track all pending permissions
|
|
@@ -29,6 +29,7 @@ function buildPermissionDedupeKey({ permission, directory, }) {
|
|
|
29
29
|
// Queue of messages waiting to be sent after current response finishes
|
|
30
30
|
// Key is threadId, value is array of queued messages
|
|
31
31
|
export const messageQueue = new Map();
|
|
32
|
+
const activeEventHandlers = new Map();
|
|
32
33
|
export function addToQueue({ threadId, message, }) {
|
|
33
34
|
const queue = messageQueue.get(threadId) || [];
|
|
34
35
|
queue.push(message);
|
|
@@ -55,7 +56,7 @@ export async function abortAndRetrySession({ sessionId, thread, projectDirectory
|
|
|
55
56
|
}
|
|
56
57
|
sessionLogger.log(`[ABORT+RETRY] Aborting session ${sessionId} for model change`);
|
|
57
58
|
// Abort with special reason so we don't show "completed" message
|
|
58
|
-
controller.abort('model-change');
|
|
59
|
+
controller.abort(new Error('model-change'));
|
|
59
60
|
// Also call the API abort endpoint
|
|
60
61
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
61
62
|
if (getClient instanceof Error) {
|
|
@@ -176,6 +177,15 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
176
177
|
if (existingController) {
|
|
177
178
|
voiceLogger.log(`[ABORT] Cancelling existing request for session: ${session.id}`);
|
|
178
179
|
existingController.abort(new Error('New request started'));
|
|
180
|
+
const abortResult = await errore.tryAsync(() => {
|
|
181
|
+
return getClient().session.abort({
|
|
182
|
+
path: { id: session.id },
|
|
183
|
+
query: { directory: sdkDirectory },
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
if (abortResult instanceof Error) {
|
|
187
|
+
sessionLogger.log(`[ABORT] Server abort failed (may be already done):`, abortResult);
|
|
188
|
+
}
|
|
179
189
|
}
|
|
180
190
|
// Auto-reject ALL pending permissions for this thread
|
|
181
191
|
const threadPermissions = pendingPermissions.get(thread.id);
|
|
@@ -210,10 +220,10 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
210
220
|
await sendThreadMessage(thread, `⚠️ ${rejectedCount} pending permission request${plural} auto-rejected due to new message`);
|
|
211
221
|
}
|
|
212
222
|
}
|
|
213
|
-
//
|
|
214
|
-
const
|
|
215
|
-
if (
|
|
216
|
-
sessionLogger.log(`[QUESTION]
|
|
223
|
+
// Answer any pending question tool with the user's message (silently, no thread message)
|
|
224
|
+
const questionAnswered = await cancelPendingQuestion(thread.id, prompt);
|
|
225
|
+
if (questionAnswered) {
|
|
226
|
+
sessionLogger.log(`[QUESTION] Answered pending question with user message`);
|
|
217
227
|
}
|
|
218
228
|
const abortController = new AbortController();
|
|
219
229
|
abortControllers.set(session.id, abortController);
|
|
@@ -230,6 +240,16 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
230
240
|
sessionLogger.log(`[DEBOUNCE] Aborted before subscribe, exiting`);
|
|
231
241
|
return;
|
|
232
242
|
}
|
|
243
|
+
const previousHandler = activeEventHandlers.get(thread.id);
|
|
244
|
+
if (previousHandler) {
|
|
245
|
+
sessionLogger.log(`[EVENT] Waiting for previous handler to finish`);
|
|
246
|
+
await Promise.race([
|
|
247
|
+
previousHandler,
|
|
248
|
+
new Promise((resolve) => {
|
|
249
|
+
setTimeout(resolve, 1000);
|
|
250
|
+
}),
|
|
251
|
+
]);
|
|
252
|
+
}
|
|
233
253
|
// Use v2 client for event subscription (has proper types for question.asked events)
|
|
234
254
|
const clientV2 = getOpencodeClientV2(directory);
|
|
235
255
|
if (!clientV2) {
|
|
@@ -252,8 +272,10 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
252
272
|
let usedAgent;
|
|
253
273
|
let tokensUsedInSession = 0;
|
|
254
274
|
let lastDisplayedContextPercentage = 0;
|
|
275
|
+
let lastRateLimitDisplayTime = 0;
|
|
255
276
|
let modelContextLimit;
|
|
256
277
|
let assistantMessageId;
|
|
278
|
+
let handlerPromise = null;
|
|
257
279
|
let typingInterval = null;
|
|
258
280
|
function startTyping() {
|
|
259
281
|
if (abortController.signal.aborted) {
|
|
@@ -509,6 +531,10 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
509
531
|
}
|
|
510
532
|
};
|
|
511
533
|
const handleSubtaskPart = async (part, subtaskInfo) => {
|
|
534
|
+
// In text-only mode, skip all subtask output (they're tool-related)
|
|
535
|
+
if (verbosity === 'text-only') {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
512
538
|
if (part.type === 'step-start' || part.type === 'step-finish') {
|
|
513
539
|
return;
|
|
514
540
|
}
|
|
@@ -700,10 +726,44 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
700
726
|
});
|
|
701
727
|
});
|
|
702
728
|
};
|
|
729
|
+
const handleSessionStatus = async (properties) => {
|
|
730
|
+
if (properties.sessionID !== session.id) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (properties.status.type !== 'retry') {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
// Throttle to once per 10 seconds
|
|
737
|
+
const now = Date.now();
|
|
738
|
+
if (now - lastRateLimitDisplayTime < 10_000) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
lastRateLimitDisplayTime = now;
|
|
742
|
+
const { attempt, message, next } = properties.status;
|
|
743
|
+
const remainingMs = Math.max(0, next - now);
|
|
744
|
+
const remainingSec = Math.ceil(remainingMs / 1000);
|
|
745
|
+
const duration = (() => {
|
|
746
|
+
if (remainingSec < 60) {
|
|
747
|
+
return `${remainingSec}s`;
|
|
748
|
+
}
|
|
749
|
+
const mins = Math.floor(remainingSec / 60);
|
|
750
|
+
const secs = remainingSec % 60;
|
|
751
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
752
|
+
})();
|
|
753
|
+
const chunk = `⬦ ${message} - retrying in ${duration} (attempt #${attempt})`;
|
|
754
|
+
await thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS });
|
|
755
|
+
};
|
|
703
756
|
const handleSessionIdle = (idleSessionId) => {
|
|
704
757
|
if (idleSessionId === session.id) {
|
|
758
|
+
// Ignore stale session.idle events - if we haven't received any content yet
|
|
759
|
+
// (no assistantMessageId set), this is likely a stale event from before
|
|
760
|
+
// the prompt was sent or from a previous request's subscription state.
|
|
761
|
+
if (!assistantMessageId) {
|
|
762
|
+
sessionLogger.log(`[SESSION IDLE] Ignoring stale idle event for ${session.id} (no content received yet)`);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
705
765
|
sessionLogger.log(`[SESSION IDLE] Session ${session.id} is idle, aborting`);
|
|
706
|
-
abortController.abort('finished');
|
|
766
|
+
abortController.abort(new Error('finished'));
|
|
707
767
|
return;
|
|
708
768
|
}
|
|
709
769
|
if (!subtaskSessions.has(idleSessionId)) {
|
|
@@ -738,6 +798,9 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
738
798
|
case 'session.idle':
|
|
739
799
|
handleSessionIdle(event.properties.sessionID);
|
|
740
800
|
break;
|
|
801
|
+
case 'session.status':
|
|
802
|
+
await handleSessionStatus(event.properties);
|
|
803
|
+
break;
|
|
741
804
|
default:
|
|
742
805
|
break;
|
|
743
806
|
}
|
|
@@ -766,7 +829,8 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
766
829
|
stopTyping();
|
|
767
830
|
stopTyping = null;
|
|
768
831
|
}
|
|
769
|
-
|
|
832
|
+
const abortReason = abortController.signal.reason?.message;
|
|
833
|
+
if (!abortController.signal.aborted || abortReason === 'finished') {
|
|
770
834
|
const sessionDuration = prettyMilliseconds(Date.now() - sessionStartTime);
|
|
771
835
|
const attachCommand = port ? ` ⋅ ${session.id}` : '';
|
|
772
836
|
const modelInfo = usedModel ? ` ⋅ ${usedModel}` : '';
|
|
@@ -835,12 +899,18 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
835
899
|
}
|
|
836
900
|
}
|
|
837
901
|
else {
|
|
838
|
-
sessionLogger.log(`Session was aborted (reason: ${
|
|
902
|
+
sessionLogger.log(`Session was aborted (reason: ${abortReason}), skipping duration message`);
|
|
839
903
|
}
|
|
840
904
|
}
|
|
841
905
|
};
|
|
842
906
|
const promptResult = await errore.tryAsync(async () => {
|
|
843
|
-
const
|
|
907
|
+
const newHandlerPromise = eventHandler().finally(() => {
|
|
908
|
+
if (activeEventHandlers.get(thread.id) === newHandlerPromise) {
|
|
909
|
+
activeEventHandlers.delete(thread.id);
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
activeEventHandlers.set(thread.id, newHandlerPromise);
|
|
913
|
+
handlerPromise = newHandlerPromise;
|
|
844
914
|
if (abortController.signal.aborted) {
|
|
845
915
|
sessionLogger.log(`[DEBOUNCE] Aborted before prompt, exiting`);
|
|
846
916
|
return;
|
|
@@ -927,7 +997,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
927
997
|
})();
|
|
928
998
|
throw new Error(`OpenCode API error (${response.response.status}): ${errorMessage}`);
|
|
929
999
|
}
|
|
930
|
-
abortController.abort('finished');
|
|
1000
|
+
abortController.abort(new Error('finished'));
|
|
931
1001
|
sessionLogger.log(`Successfully sent prompt, got response`);
|
|
932
1002
|
if (originalMessage) {
|
|
933
1003
|
const reactionResult = await errore.tryAsync(async () => {
|
|
@@ -940,6 +1010,14 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
940
1010
|
}
|
|
941
1011
|
return { sessionID: session.id, result: response.data, port };
|
|
942
1012
|
});
|
|
1013
|
+
if (handlerPromise) {
|
|
1014
|
+
await Promise.race([
|
|
1015
|
+
handlerPromise,
|
|
1016
|
+
new Promise((resolve) => {
|
|
1017
|
+
setTimeout(resolve, 1000);
|
|
1018
|
+
}),
|
|
1019
|
+
]);
|
|
1020
|
+
}
|
|
943
1021
|
if (!errore.isError(promptResult)) {
|
|
944
1022
|
return promptResult;
|
|
945
1023
|
}
|
|
@@ -948,7 +1026,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
948
1026
|
return;
|
|
949
1027
|
}
|
|
950
1028
|
sessionLogger.error(`ERROR: Failed to send prompt:`, promptError);
|
|
951
|
-
abortController.abort('error');
|
|
1029
|
+
abortController.abort(new Error('error'));
|
|
952
1030
|
if (originalMessage) {
|
|
953
1031
|
const reactionResult = await errore.tryAsync(async () => {
|
|
954
1032
|
await originalMessage.reactions.removeAll();
|
package/dist/tools.js
CHANGED
|
@@ -6,9 +6,9 @@ import { z } from 'zod';
|
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import net from 'node:net';
|
|
8
8
|
import { createOpencodeClient, } from '@opencode-ai/sdk';
|
|
9
|
-
import { createLogger } from './logger.js';
|
|
9
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
10
10
|
import * as errore from 'errore';
|
|
11
|
-
const toolsLogger = createLogger(
|
|
11
|
+
const toolsLogger = createLogger(LogPrefix.TOOLS);
|
|
12
12
|
import { ShareMarkdown } from './markdown.js';
|
|
13
13
|
import { formatDistanceToNow } from './utils.js';
|
|
14
14
|
import pc from 'picocolors';
|
package/dist/voice-handler.js
CHANGED
|
@@ -17,8 +17,8 @@ import { getDatabase } from './database.js';
|
|
|
17
17
|
import { sendThreadMessage, escapeDiscordFormatting, SILENT_MESSAGE_FLAGS, } from './discord-utils.js';
|
|
18
18
|
import { transcribeAudio } from './voice.js';
|
|
19
19
|
import { FetchError } from './errors.js';
|
|
20
|
-
import { createLogger } from './logger.js';
|
|
21
|
-
const voiceLogger = createLogger(
|
|
20
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
21
|
+
const voiceLogger = createLogger(LogPrefix.VOICE);
|
|
22
22
|
export const voiceConnections = new Map();
|
|
23
23
|
export function convertToMono16k(buffer) {
|
|
24
24
|
const inputSampleRate = 48000;
|
package/dist/voice.js
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
// Uses errore for type-safe error handling.
|
|
5
5
|
import { GoogleGenAI, Type } from '@google/genai';
|
|
6
6
|
import * as errore from 'errore';
|
|
7
|
-
import { createLogger } from './logger.js';
|
|
7
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
8
8
|
import { glob } from 'glob';
|
|
9
9
|
import { ripGrep } from 'ripgrep-js';
|
|
10
10
|
import { ApiKeyMissingError, InvalidAudioFormatError, TranscriptionError, EmptyTranscriptionError, NoResponseContentError, NoToolResponseError, GrepSearchError, GlobSearchError, } from './errors.js';
|
|
11
|
-
const voiceLogger = createLogger(
|
|
11
|
+
const voiceLogger = createLogger(LogPrefix.VOICE);
|
|
12
12
|
function runGrep({ pattern, directory }) {
|
|
13
13
|
return errore.tryAsync({
|
|
14
14
|
try: async () => {
|